From 1a72859bd8aab31a9d32d3af1dd6225c089e9db8 Mon Sep 17 00:00:00 2001 From: Bryce Covert Date: Wed, 23 May 2018 00:30:04 -0700 Subject: [PATCH] created a typeahead. neat. --- project.clj | 2 + resources/public/index.html | 47 ++++++++++++ src/cljs/auto_ap/events.cljs | 25 ++++--- .../auto_ap/views/pages/unpaid_invoices.cljs | 71 ++++++++++++++++--- src/cljs/auto_ap/views/utils.cljs | 13 +++- 5 files changed, 138 insertions(+), 20 deletions(-) diff --git a/project.clj b/project.clj index c453294a..d1b17e90 100644 --- a/project.clj +++ b/project.clj @@ -92,6 +92,8 @@ :jar true :compiler {:main auto-ap.core :output-to "resources/public/js/compiled/app.js" + :npm-deps {:react-autocomplete "1.8.1"} + :install-deps true :optimizations :advanced :closure-defines {goog.DEBUG false} :pretty-print false}} diff --git a/resources/public/index.html b/resources/public/index.html index a7c57a9a..a0dc33a4 100644 --- a/resources/public/index.html +++ b/resources/public/index.html @@ -247,6 +247,53 @@ background-color:#F5F5F5; } .table { table-layout: fixed } + + .typeahead { + position:relative; + } + +.typeahead-menu { + position: absolute; + display: inline-block; + width: 100%; + top: 100%; + left: 0; + z-index: 1000; + float: left; + min-width: 160px; + padding: 5px 0; + margin: 2px 0 0; + list-style: none; + font-size: 14px; + text-align: left; + background-color: #ffffff; + border: 1px solid #cccccc; + border: 1px solid rgba(0, 0, 0, 0.15); + border-radius: 4px; + -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); + box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); + background-clip: padding-box; +} + +.typeahead-suggestion { + display: block; + padding: 3px 20px; + clear: both; + font-weight: normal; + line-height: 1.42857143; + color: #333333; + white-space: nowrap; +} +.typeahead-suggestion:hover, +.typeahead-suggestion:focus, + .typeahead-menu:not(:hover) .typeahead-highlighted + { + color: #ffffff; + text-decoration: none; + outline: 0; + background-color: #00d1b2; + cursor: pointer; +} diff --git a/src/cljs/auto_ap/events.cljs b/src/cljs/auto_ap/events.cljs index 4c66141f..f9719399 100644 --- a/src/cljs/auto_ap/events.cljs +++ b/src/cljs/auto_ap/events.cljs @@ -4,6 +4,7 @@ [auto-ap.subs :as subs] [auto-ap.routes :as routes] [auto-ap.effects :as effects] + [auto-ap.utils :refer [by]] [venia.core :as v] [bidi.bidi :as bidi] [goog.crypt.base64 :as b64] @@ -35,9 +36,11 @@ :user token) :graphql {:token token :query-obj {:venia/queries [[:company - [:id :name [:bank-accounts [:id :number :check-number :name]]]]]} + [:id :name [:bank-accounts [:id :number :check-number :name]]]] + [:vendor + [:id :name]]]} - :on-success [::received-companies]}})))) + :on-success [::received-initial]}})))) (re-frame/reg-event-db ::toggle-menu @@ -49,19 +52,19 @@ (fn [{:keys [db]} [_ token user]] {:graphql {:token token :query-obj {:venia/queries [[:company - [:id :name]]]} + [:id :name [:bank-accounts [:id :number :check-number :name]]]] + [:vendor + [:id :name]]]} - :on-success [::received-companies]} + :on-success [::received-initial]} :db (assoc db :user (assoc user :token token))})) (re-frame/reg-event-db - ::received-companies - (fn [db [_ {companies :company}]] - - (assoc db :companies (reduce (fn [companies company] - (assoc companies (:id company) company)) - {} - companies)))) + ::received-initial + (fn [db [_ {companies :company vendors :vendor :as x}]] + (-> db + (assoc :companies (by :id companies) ) + (assoc :vendors (by :id vendors) )))) (re-frame/reg-event-db ::swap-company diff --git a/src/cljs/auto_ap/views/pages/unpaid_invoices.cljs b/src/cljs/auto_ap/views/pages/unpaid_invoices.cljs index 5a04b4aa..4544f4ba 100644 --- a/src/cljs/auto_ap/views/pages/unpaid_invoices.cljs +++ b/src/cljs/auto_ap/views/pages/unpaid_invoices.cljs @@ -1,5 +1,7 @@ (ns auto-ap.views.pages.unpaid-invoices (:require [re-frame.core :as re-frame] + [reagent.core :as r] + [clojure.string :as str] [auto-ap.entities.companies :as company] [auto-ap.entities.vendors :as vendor] [auto-ap.views.utils :refer [dispatch-event bind-field horizontal-field]] @@ -238,9 +240,60 @@ :max outstanding-balance :step "0.01"}]]]]]])]]]))) +(defn typeahead [{:keys [matches on-change field value]}] + (let [text (r/atom (or (second (first (filter #(= (first %) value) matches))) "")) + highlighted (r/atom 0) + selected (r/atom (first (first (filter #(= (first %) value) matches)))) + select (fn [[id t]] + (reset! selected id) + (reset! text t) + (println [id t]) + (when on-change + (on-change id)))] + (fn [{:keys [matches on-change field value]}] + (let [valid-matches (take 5 (for [[[id t :as match] i] (map vector matches (range)) + :when (str/includes? (.toLowerCase t) (.toLowerCase @text))] + match))] + [:div.typeahead + [:input.input {:type "text" + :field [:vendor] + :value @text + :on-blur (fn [e] + (cond @selected + nil + + (seq valid-matches) + (do (select (first valid-matches)) + true) + + :else + (do (select [nil ""]) + true)) + ) + :on-key-up (fn [e] + (if (= 13 (.-keyCode e)) + (do + (select (first valid-matches)) + false) + true)) + :on-change (fn [e] + (reset! highlighted (ffirst valid-matches)) + (select [nil (.. e -target -value)]))}] + (when (and (seq @text) + (not @selected) + (seq valid-matches)) + [:div.typeahead-menu + [:ul + (for [[id t :as match] valid-matches] + + [:li.typeahead-suggestion {:class (if (= id @highlighted) + "typeahead-highlighted") + :on-mouse-down #(do (println "MATCH" match) (select match))} t])]])])))) + (defn new-invoice-modal [] (let [data @(re-frame/subscribe [::new-invoice]) change-event [::events/change-form [::new-invoice]]] + (println data) [action-modal {:id ::new-invoice :title "New Invoice" :action-text "Create" @@ -248,10 +301,11 @@ [horizontal-field [:label.label "Vendor"] [bind-field - [:input.input {:type "text" - :field [:vendor] - :event change-event - :subscription data}]]] + [typeahead {:matches (map (fn [x] [(:id x) (:name x)]) @(re-frame/subscribe [::subs/vendors])) + :type "typeahead" + :field [:vendor-id] + :event change-event + :subscription data}]]] [horizontal-field [:label.label "Date"] [bind-field @@ -263,10 +317,11 @@ [horizontal-field [:label.label "Company"] [bind-field - [:input.input {:type "text" - :field [:company] - :event change-event - :subscription data}]]] + [typeahead {:matches (map (fn [x] [(:id x) (:name x)]) @(re-frame/subscribe [::subs/companies])) + :type "typeahead" + :field [:company-id] + :event change-event + :subscription data}]]] [horizontal-field [:label.label "Invoice #"] diff --git a/src/cljs/auto_ap/views/utils.cljs b/src/cljs/auto_ap/views/utils.cljs index 587e01d7..ae32ae8f 100644 --- a/src/cljs/auto_ap/views/utils.cljs +++ b/src/cljs/auto_ap/views/utils.cljs @@ -68,11 +68,22 @@ keys (dissoc keys :field :subscription :event :spec)] (into [dom keys] (with-keys rest)))) +(defmethod do-bind "typeahead" [dom {:keys [field event subscription class spec] :as keys} & rest] + (let [field (if (keyword? field) [field] field) + event (if (keyword? event) [event] event) + keys (assoc keys + :on-change (fn [selected] + (re-frame/dispatch (conj (conj event field) selected))) + :value (get-in subscription field) + :class (str class + (when (and spec (not (s/valid? spec (get-in subscription field)))) + " is-danger"))) + keys (dissoc keys :field :subscription :event :spec)] + (into [dom keys] (with-keys rest)))) (defmethod do-bind :default [dom {:keys [field event subscription class spec] :as keys} & rest] (let [field (if (keyword? field) [field] field) event (if (keyword? event) [event] event) - _ (println field event dom rest) keys (assoc keys :on-change (dispatch-value-change (conj event field)) :value (get-in subscription field)