mirror of
https://github.com/metosin/reitit.git
synced 2025-12-16 16:01:11 +00:00
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:
parent
d54c05426c
commit
08156f6a6d
4 changed files with 249 additions and 147 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)))
|
||||
|
|
|
|||
|
|
@ -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))))
|
||||
|
|
|
|||
|
|
@ -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})]))))
|
||||
|
|
|
|||
Loading…
Reference in a new issue