-
Notifications
You must be signed in to change notification settings - Fork 23
/
autocomplete.cljc
169 lines (159 loc) · 9.97 KB
/
autocomplete.cljc
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
(ns com.fulcrologic.rad.rendering.semantic-ui.autocomplete
(:require
#?@(:cljs
[[com.fulcrologic.fulcro.dom :as dom :refer [div label input]]
[goog.object :as gobj]
[cljs.reader :refer [read-string]]
[com.fulcrologic.semantic-ui.modules.dropdown.ui-dropdown :refer [ui-dropdown]]]
:clj
[[com.fulcrologic.fulcro.dom-server :as dom :refer [div label input]]])
[clojure.string :as str]
[com.fulcrologic.fulcro.algorithms.merge :as merge]
[com.fulcrologic.fulcro.algorithms.normalized-state :as fns]
[com.fulcrologic.fulcro.components :as comp :refer [defsc]]
[com.fulcrologic.fulcro.data-fetch :as df]
[com.fulcrologic.fulcro.mutations :as m :refer [defmutation]]
[com.fulcrologic.fulcro.rendering.multiple-roots-renderer :as mroot]
[com.fulcrologic.rad.attributes :as attr]
[com.fulcrologic.rad.form :as form]
[com.fulcrologic.rad.form-options :as fo]
[com.fulcrologic.rad.ids :as ids]
[com.fulcrologic.rad.options-util :as opts]
[com.fulcrologic.rad.rendering.semantic-ui.form-options :as sufo]
[taoensso.timbre :as log]))
(defsc AutocompleteQuery [_ _] {:query [:text :value]})
(defn to-js [v]
#?(:clj v
:cljs (clj->js v)))
(defmutation normalize-options [{:keys [source target]}]
(action [{:keys [state]}]
#?(:clj true
:cljs
(let [options (get @state source)
normalized-options (apply array
(map (fn [{:keys [text value]}]
#js {:text text :value (pr-str value)}) options))]
(fns/swap!-> state
(dissoc source)
(assoc-in target normalized-options))))))
(defsc AutocompleteField [this {:ui/keys [search-string options] :as props} {:keys [value label onChange
invalid? validation-message
className omit-label?
read-only?]}]
{:initLocalState (fn [this]
;; Possible problem???: props not making it...fix that, or debounce isn't configurable.
(let [{:autocomplete/keys [debounce-ms]} (comp/props this)]
{:load! (opts/debounce
(fn [s]
(let [{id ::autocomplete-id
:autocomplete/keys [search-key]} (comp/props this)]
(df/load! this search-key AutocompleteQuery
{:params {:search-string s}
:post-mutation `normalize-options
:post-mutation-params {:source search-key
:target [::autocomplete-id id :ui/options]}})))
(or debounce-ms 200))}))
:componentDidMount (fn [this]
(let [{id ::autocomplete-id
:autocomplete/keys [search-key preload?]} (comp/props this)
value (comp/get-computed this :value)]
(cond
preload? (df/load! this search-key AutocompleteQuery
{:post-mutation `normalize-options
:post-mutation-params {:source search-key
:target [::autocomplete-id id :ui/options]}})
(and search-key value) (df/load! this search-key AutocompleteQuery
{:params {:only value}
:post-mutation `normalize-options
:post-mutation-params {:source search-key
:target [::autocomplete-id id :ui/options]}}))))
:query [::autocomplete-id :ui/search-string :ui/options :autocomplete/search-key
:autocomplete/debounce-ms :autocomplete/minimum-input]
:ident ::autocomplete-id}
(let [load! (comp/get-state this :load!)]
#?(:clj
(dom/div "")
:cljs
(dom/div :.field {:className (or className "field")
:classes [(when invalid? "error")]}
(when-not omit-label?
(dom/label label (when invalid? (dom/span " " validation-message))))
(if read-only?
(gobj/getValueByKeys options 0 "text")
(ui-dropdown #js {:search true
:options (if options options #js [])
:value (pr-str value)
:selection true
:closeOnBlur true
:openOnFocus true
:selectOnBlur true
:selectOnNavigation true
:onSearchChange (fn [_ v]
(let [query (comp/isoget v "searchQuery")]
(load! query)))
:onChange (fn [_ v]
(when onChange
(onChange (some-> (comp/isoget v "value")
read-string))))}))
(when (and invalid? omit-label?)
(dom/div :.red validation-message))))))
(def ui-autocomplete-field (comp/computed-factory AutocompleteField {:keyfn ::autocomplete-id}))
(defmutation gc-autocomplete [{:keys [id]}]
(action [{:keys [state]}]
(when id
(swap! state fns/remove-entity [::autocomplete-id id]))))
(defsc AutocompleteFieldRoot [this props {:keys [env attribute]}]
{:initLocalState (fn [this] {:field-id (ids/new-uuid)})
:componentDidMount (fn [this]
(let [id (comp/get-state this :field-id)
{:keys [env attribute]} (comp/get-computed this)
{::form/keys [form-instance]} env
{:autocomplete/keys [search-key debounce-ms minimum-input]} (fo/get-field-options (comp/component-options form-instance) attribute)]
(merge/merge-component! this AutocompleteField {::autocomplete-id id
:autocomplete/search-key search-key
:autocomplete/debounce-ms debounce-ms
:autocomplete/minimum-input minimum-input
:ui/search-string ""
:ui/options #js []}))
(mroot/register-root! this {:initialize? true}))
:shouldComponentUpdate (fn [_ _] true)
:initial-state {::autocomplete-id {}}
:componentWillUnmount (fn [this]
(comp/transact! this [(gc-autocomplete {:id (comp/get-state this :field-id)})])
(mroot/deregister-root! this))
:query [::autocomplete-id]}
(let [{:autocomplete/keys [debounce-ms search-key preload?]} (fo/get-field-options (comp/component-options this) attribute)
k (::attr/qualified-key attribute)
{::form/keys [form-instance]} env
top-class (sufo/top-class form-instance attribute)
value (-> (comp/props form-instance) (get k))
id (comp/get-state this :field-id)
label (form/field-label env attribute)
read-only? (form/read-only? form-instance attribute)
omit-label? (form/omit-label? form-instance attribute)
invalid? (form/invalid-attribute-value? env attribute)
validation-message (when invalid? (form/validation-error-message env attribute))
field (get-in props [::autocomplete-id id])]
;; Have to pass the id and debounce early since the merge in mount won't happen until after, which is too late for initial
;; state
(ui-autocomplete-field (assoc field
::autocomplete-id id
:autocomplete/search-key search-key
:autocomplete/preload? preload?
:autocomplete/debounce-ms debounce-ms)
(cond-> {:value value
:invalid? invalid?
:validation-message validation-message
:label label
:read-only? read-only?
:omit-label? omit-label?
:onChange (fn [normalized-value]
#?(:cljs
(when normalized-value (form/input-changed! env k normalized-value))))}
top-class (assoc :className top-class)))))
(def ui-autocomplete-field-root (mroot/floating-root-factory AutocompleteFieldRoot
{:keyfn (fn [props] (-> props :attribute ::attr/qualified-key))}))
(defn render-autocomplete-field [env {::attr/keys [cardinality] :or {cardinality :one} :as attribute}]
(if (= :many cardinality)
(log/error "Cannot autocomplete to-many attributes with renderer" `render-autocomplete-field)
(ui-autocomplete-field-root {:env env :attribute attribute})))