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 []
[:div
[:h2 "Welcome to frontend"]
[:button
{:type "button"
:on-click #(rfe/set-token ::item {:id 3})}
"Item 3"]])
:on-click #(rfe/push-state ::item {:id 3})}
"Item 3"]
[:button
{:type "button"
:on-click #(rfe/replace-state ::item {:id 4})}
"Replace State Item 4"] ])
(defn about-page []
[:div

View file

@ -6,7 +6,20 @@
(defonce history (atom nil))
;; Doc-strings from reitit.frontend.history
;; remember to update both!
(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]
(swap! history (fn [old-history]
(rfh/stop! old-history)
@ -14,26 +27,26 @@
(defn href
([k]
(rfh/href @history k))
(rfh/href @history k nil nil))
([k params]
(rfh/href @history k params))
(rfh/href @history k params nil))
([k params query]
(rfh/href @history k params query)))
(defn set-token
(defn push-state
"Sets the new route, leaving previous route in history."
([k]
(rfh/set-token @history k))
(rfh/push-state @history k nil nil))
([k params]
(rfh/set-token @history k params))
(rfh/push-state @history k params nil))
([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."
([k]
(rfh/replace-token @history k))
(rfh/replace-state @history k nil nil))
([k params]
(rfh/replace-token @history k params))
(rfh/replace-state @history k params nil))
([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]
[reitit.core :as r]
[reitit.frontend :as rf]
[reitit.impl :as impl])
(:import goog.history.Html5History
goog.Uri))
[reitit.impl :as impl]
[goog.events :as gevents])
(:import goog.Uri))
;; Token is for Closure HtmlHistory
;; Path is for reitit
(defprotocol History
(-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]
(if (.-useFragment_ history)
;; If no fragment at all, default to "/"
;; If fragment is present, the token already is prefixed with "/"
(if (= "" token)
(.getPathPrefix history)
token)
(str (.getPathPrefix history) token)))
;; This version listens for both pop-state and hash-change for
;; compatibility for old browsers not supporting History API.
(defrecord FragmentHistory [on-navigate router listen-key last-fragment]
History
(-init [this]
;; Link clicks and e.g. back button trigger both events, if fragment is same as previous ignore second event.
;; For old browsers only the hash-change event is triggered.
(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]
(subs path (if (.-useFragment_ history)
1
(count (.getPathPrefix history)))))
(defrecord Html5History [on-navigate router listen-key click-listen-key]
History
(-init [this]
(let [handler
(fn [e]
(-on-navigate this (-get-path this)))
(defn- token->href [history token]
(if token
(str (if (.-useFragment_ history)
(str "#"))
(.getPathPrefix history)
token)))
current-domain
(if (exists? js/location)
(.getDomain (.parse Uri js/location)))
(def ^:private current-domain (if (exists? js/location)
(.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)
(.setToken history (path->token history (str (.getPath uri)
(if (seq (.getQuery uri))
(str "?" (.getQuery uri))))))))))
(impl/goog-extend
^{:jsdoc ["@constructor"
"@extends {Html5History.TokenTransformer}"]}
TokenTransformer
Html5History.TokenTransformer
([]
(this-as this
(.call Html5History.TokenTransformer this)))
(retrieveToken [path-prefix location]
(subs (.-pathname location) (count path-prefix)))
(createUrl [token path-prefix location]
;; Code in Closure also adds current query params
;; from location.
(str path-prefix token)))
;; Prevent document load when clicking a elements, if the href points to URL that is part
;; of the routing tree."
ignore-anchor-click
(fn ignore-anchor-click
[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)
(let [path (str (.getPath uri)
(if (seq (.getQuery uri))
(str "?" (.getQuery uri))))]
(.pushState js/window.history nil "" path)
(-on-navigate this path))))))]
(-on-navigate this (-get-path this))
(assoc this
:listen-key (gevents/listen js/window goog.events.EventType.POPSTATE handler false)
:click-listen-key (e/listen js/document e/EventType.CLICK ignore-anchor-click))))
(-on-navigate [this path]
(on-navigate (rf/match-by-path router path) this))
(-stop [this]
(gevents/unlistenByKey listen-key)
(gevents/unlistenByKey click-listen-key))
(-get-path [this]
(.. js/window -location -pathname))
(-href [this path]
path))
(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
remove listeners using stop! call before calling start! again.
Parameters:
- router The reitit routing tree.
- on-navigate Function to be called when route changes.
- router The Reitit routing tree.
- on-navigate Function to be called when route changes. Takes two parameters, ´match´ and ´history´ object.
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. nil (TokenTransformer.))
(.setEnabled true)
(.setPathPrefix path-prefix)
(.setUseFragment use-fragment))
- :use-fragment (default true) If true, onhashchange and location hash are used to store current route."
([router on-navigate]
(start! router on-navigate nil))
([router
on-navigate
{:keys [use-fragment]
:or {use-fragment true}}]
(let [opts {:router router
:on-navigate on-navigate}]
(-init (if use-fragment
(map->FragmentHistory opts)
(map->Html5History opts))))))
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)
(.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 stop! [history]
(if history
(-stop history)))
(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 query)]
(token->href history token))))
([history k]
(href history k nil))
([history k params]
(href history k params nil))
([history k params query]
(let [match (rf/match-by-name! (:router history) k params)]
(-href history (r/match->path match query)))))
(defn set-token
(defn push-state
"Sets the new route, leaving previous route in history."
([state k]
(set-token state k nil))
([state k params]
(set-token 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 query)]
(.setToken history token))))
([history k]
(push-state history k nil nil))
([history k params]
(push-state history k params nil))
([history k params query]
(let [match (rf/match-by-name! (:router history) k params)
path (r/match->path match query)]
;; 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."
([state k]
(replace-token state k nil))
([state k params]
(replace-token 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 query)]
(.replaceToken history token))))
([history k]
(replace-state history k nil nil))
([history k params]
(replace-state history k params nil))
([history k params query]
(let [match (rf/match-by-name! (:router history) k params)
path (r/match->path match query)]
(.replaceState js/window.history nil "" (-href history path))
(-on-navigate history path))))

View file

@ -1,9 +1,10 @@
(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.frontend :as rf]
[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))
@ -14,6 +15,9 @@
(deftest fragment-history-test
(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})]
(testing "creating urls"
@ -29,10 +33,48 @@
(is (= nil value))
(is (= [{:type :warn
: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
(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})]
(testing "creating urls"
@ -48,4 +90,40 @@
(is (= nil value))
(is (= [{:type :warn
: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})]))))