-
Notifications
You must be signed in to change notification settings - Fork 10
/
push_state.clj
84 lines (75 loc) · 3.31 KB
/
push_state.clj
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
(ns ripley.live.push-state
"Helpers for working with history push state."
(:require [ripley.html :as h]
[ripley.live.protocols :as p]
[ripley.impl.dynamic :as dyn]
[clojure.string :as str]))
(defn- encode [x]
(java.net.URLEncoder/encode (str x)))
(defn- decode [x]
(java.net.URLDecoder/decode x))
(defn- ->js
"Encode value suitably for js string."
[val]
(encode (pr-str val)))
(defn- ->clj
"Read value to Clojure data"
[str]
(binding [*read-eval* false]
(read-string (decode str))))
(defn- push-state-query-js [push-state-fn {path ::path :as val}]
(let [args (dissoc val ::path)
query-string (when (seq args)
(str "?"
(str/join
"&"
(for [[k v] args]
(str (if (keyword? k)
(name k)
(str k))
"="
(java.net.URLEncoder/encode (str v)))))))
location (if path
(str "\"" path query-string "\"")
(str "location.pathname+\"" query-string "\""))]
(str push-state-fn "(\"" (->js val) "\"," location ")")))
(defn push-state
"Outputs a script that sets path and query parameters based on source value (a map).
When browser pops state, the callback is invoked to handle new values.
The callback will be invoked with the popped state.
The location path is included in the map with key :ripley.live.push-state/path
all other keys are considered to be query parameters.
If on-pop-state is omitted, the default will write the value to the
source.
"
([path-and-params-source]
(push-state path-and-params-source
#(p/write! path-and-params-source %)))
([path-and-params-source on-pop-state]
(let [push-state-fn (str (gensym "_ps"))
ctx dyn/*live-context*
last-popped (atom nil)
component-id (p/register! ctx path-and-params-source
(partial push-state-query-js push-state-fn)
{:patch :eval-js
:should-update? (fn [val]
(and
(not (::popped? (meta val)))
(not= val @last-popped)))})
callback-id (p/register-callback!
ctx
(fn [arg]
(let [val (with-meta (->clj arg) {::popped? true})]
(reset! last-popped val)
(on-pop-state val))))]
(h/out! "<script data-rl=\"" component-id "\">\n"
"history.replaceState({s:\"" (->js (p/current-value path-and-params-source)) "\"},document.title)\n"
"function " push-state-fn "(state,location) {\n"
"var l = window.location; "
"window.history.pushState({s:state},\"\","
"l.protocol+\"//\"+l.host+location)\n"
"}\n"
"window.addEventListener(\"popstate\", (s) => ripley.send(" callback-id ",[s.state.s]))\n"
"</script>\n"))))
;; Allow call with old name
(def push-state-query push-state)