-
Notifications
You must be signed in to change notification settings - Fork 10
/
js.clj
219 lines (186 loc) · 7.71 KB
/
js.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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
(ns ripley.js
"JavaScript helpers"
(:require [ripley.live.protocols :as p]
[ripley.impl.dynamic :as dyn]
[ripley.html :as h]
[clojure.string :as str]
[ripley.impl.util :refer [arity]]))
(defn- wrap-success [fun]
(case (arity fun)
0 (fn wrap-success-0 [] {:ripley/success (fun)})
1 (fn wrap-success-1 [a] {:ripley/success (fun a)})
2 (fn wrap-success-2 [a b] {:ripley/success (fun a b)})
3 (fn wrap-success-3 [a b c] {:ripley/success (fun a b c)})
4 (fn wrap-success-4 [a b c d] {:ripley/success (fun a b c d)})
5 (fn wrap-success-5 [a b c d e] {:ripley/success (fun a b c d e)})
6 (fn wrap-success-6 [a b c d e f] {:ripley/success (fun a b c d e f)})
7 (fn wrap-success-7 [a b c d e f g] {:ripley/success (fun a b c d e f g)})
(fn wrap-success-n [& args]
{:ripley/success (apply fun args)})))
(defn- wrap-failure-call [fun args]
(try
(apply fun args)
(catch Throwable t
(throw (ex-info (.getMessage t)
(merge (ex-data t)
{:ripley/failure true}))))))
(defn- wrap-failure [fun]
(case (arity fun)
0 (fn wrap-failure-0 [] (wrap-failure-call fun []))
1 (fn wrap-failure-1 [a] (wrap-failure-call fun [a]))
2 (fn wrap-failure-2 [a b] (wrap-failure-call fun [a b]))
3 (fn wrap-failure-3 [a b c] (wrap-failure-call fun [a b c]))
4 (fn wrap-failure-4 [a b c d] (wrap-failure-call fun [a b c d]))
5 (fn wrap-failure-5 [a b c d e] (wrap-failure-call fun [a b c d e]))
6 (fn wrap-failure-6 [a b c d e f] (wrap-failure-call fun [a b c d e f]))
7 (fn wrap-failure-7 [a b c d e f g] (wrap-failure-call fun [a b c d e f g]))
(fn wrap-failure-n [& args] (wrap-failure-call fun args))))
(defn- wrap-ignore-success-call [fun args]
(apply fun args)
{:ripley/success true})
;; For cases there exists a failure handler, but no success handler
;; and the callback doesn't fail... we still need to notify client
;; so that it removes the handler from its state.
(defn wrap-ignore-success [fun]
(case (arity fun)
0 (fn wrap-ignore-success-0 [] (wrap-ignore-success-call fun []))
1 (fn wrap-ignore-success-1 [a] (wrap-ignore-success-call fun [a]))
2 (fn wrap-ignore-success-2 [a b] (wrap-ignore-success-call fun [a b]))
3 (fn wrap-ignore-success-3 [a b c] (wrap-ignore-success-call fun [a b c]))
4 (fn wrap-ignore-success-4 [a b c d] (wrap-ignore-success-call fun [a b c d]))
5 (fn wrap-ignore-success-5 [a b c d e] (wrap-ignore-success-call fun [a b c d e]))
6 (fn wrap-ignore-success-6 [a b c d e f] (wrap-ignore-success-call fun [a b c d e f]))
7 (fn wrap-ignore-success-7 [a b c d e f g] (wrap-ignore-success-call fun [a b c d e f g]))
(fn [& args]
(wrap-ignore-success-call fun args))))
(defrecord JSCallback [callback-fn condition js-params debounce-ms
on-success on-failure]
p/Callback
(callback-js-params [_] js-params)
(callback-fn [_]
(cond-> callback-fn
on-success
(wrap-success)
on-failure
(wrap-failure)
(and on-failure (not on-success))
(wrap-ignore-success)))
(callback-debounce-ms [_] debounce-ms)
(callback-condition [_] condition)
(callback-on-success [_] on-success)
(callback-on-failure [_] on-failure))
(defn js
"Create a JavaScript callback that evaluates JS in browser to get parameters"
[callback-fn & js-params]
(map->JSCallback {:callback-fn callback-fn :js-params js-params}))
(defn js-when
"Create a conditionally fired JS callback."
[js-condition callback-fn & js-params]
(map->JSCallback {:callback-fn callback-fn
:condition js-condition
:js-params js-params}))
(defn js-debounced
"Create callback that sends value only when parameters settle (aren't
changed within given ms). This is useful for input values to prevent
sending on each keystroke, only when user stops typing."
[debounce-ms callback-fn & js-params]
(map->JSCallback {:callback-fn callback-fn
:js-params js-params
:debounce-ms debounce-ms}))
(defn keycode-pressed?
"Return JS code for checking if keypress event has given keycode"
[keycode]
(str "window.event.keyCode == " keycode))
(def enter-pressed?
"JS for checking if enter was pressed"
(keycode-pressed? 13))
(def esc-pressed?
"JS for checking if escape was pressed"
(keycode-pressed? 27))
(def change-value
"JavaScript for current on-change event value"
"window.event.target.value")
(def keycode
"JavaScript for current event keycode"
"window.event.keyCode")
(def keycode-char
"String.fromCharCode(window.event.keyCode)")
(defn input-value
"Generate JS for getting the value of an input field by id."
[id]
(str "document.getElementById('" (if (keyword? id)
(name id)
id) "').value"))
(def prevent-default
"window.event.preventDefault()")
(defn form-values
"Extract form values as a map of key/value."
[form-selector]
(str "(()=>{"
"let d = {};"
"for(const e of new FormData(document.querySelector('" form-selector "')).entries())"
"d[e[0]]=e[1];"
"return d;"
"})()"))
(defn eval-js-from-source
"Output a script element that evaluates JS code from source.
Evaluates JS whenever source changes. The source values must be valid
scripts."
[source]
(let [id (p/register! dyn/*live-context* source
identity
{:patch :eval-js})]
(h/html [:script {:data-rl id}])))
(defn- with [callback field value]
(map->JSCallback (merge (if (fn? callback)
{:callback-fn callback}
callback)
{field value})))
(defn on-success
"Add JS code that is run after the callback is processed on the server."
[callback on-success-js]
{:pre [(string? on-success-js)]}
(with callback :on-success on-success-js))
(defn on-failure
"Add JS code that handles callback failure."
[callback on-failure-js]
{:pre [(string? on-failure-js)]}
(with callback :on-failure on-failure-js))
(defn export-callbacks
"Output a JS script tag that exposes the given callbacks as global
JS functions or inside a global object.
Names must be keywords denoting valid JS function names.
eg. `(export-callbacks {:foo #(println \"Hello\" %)})`
will create a JS function called `foo` that takes 1 argument
and invokes the server side callback with it.
"
([js-name->callback] (export-callbacks nil js-name->callback))
([object-name js-name->callback]
(h/out! "<script>\n")
(when object-name
(h/out! (name object-name) "={"))
(doall
(map-indexed
(fn [i [js-name callback]]
(let [fn-name (name js-name)
callback-fn (cond
(fn? callback) callback
(satisfies? p/Callback callback)
(p/callback-fn callback)
:else (throw (ex-info "Must be function or Callback record"
{:unexpected-callback callback})))
argc (arity callback-fn)
args (map #(str (char (+ 97 %))) (range argc))
cb (if (fn? callback)
(apply js callback args)
(assoc callback :js-params args))]
(if object-name
(h/out! (when (pos? i) ",") "\n"
fn-name ": function(" (str/join "," args) "){"
(h/register-callback cb)
"}")
(h/out! "function " fn-name "(" (str/join "," args) "){"
(h/register-callback cb)
"}\n"))))
js-name->callback))
(h/out! (when object-name "}") "</script>")))