mirror of
https://github.com/metosin/reitit.git
synced 2025-12-18 08:51:12 +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 []
|
(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
|
||||||
|
|
|
||||||
|
|
@ -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)))
|
||||||
|
|
|
||||||
|
|
@ -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))))
|
||||||
|
|
|
||||||
|
|
@ -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})]))))
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue