keechma-forms-example

0.1.0-SNAPSHOT


dependencies

org.clojure/clojure
1.7.0
org.clojure/clojurescript
1.7.228
json-html
0.4.0
reagent
0.5.1
(this space intentionally left almost blank)
 
(ns keechma-forms-example.core
  (:require [reagent.core :as reagent]
            [forms.core :as f]
            [forms.validator :as v]
            [json-html.core :as j]))

Shared validator definitions

(def validations
  {:not-empty {:message "can't be empty"
               :validator (fn [v] (not (empty? v)))}
   :valid-email {:message "is not valid email"
                 :validator (fn [v] (not= -1 (.indexOf (or v "") "@")))}
   :long-enough {:message "is too short"
                 :validator (fn [v] (> (count v) 6))}})

Helper function that extracts the validator definitions.

(defn to-validator
  [validations config]
  (reduce-kv (fn [m attr v]
               (assoc m attr
                      (map (fn [k] [k (get-in validations [k :validator])]) v))) {} config))

Defines the validator for the form.

(def validator
  (v/validator
   (to-validator validations
                 {:username [:not-empty]
                  :name [:not-empty]
                  :email [:valid-email]
                  :password [:long-enough]
                  :accounts.*.username [:not-empty] ;; Validate username for each account
                  :accounts.*.network [:not-empty] ;; Validate network for each account
                  :accounts [:not-empty] ;; Account list must have entries
                  :phone-numbers.* [:not-empty]}))) ;; Validate each phone number in a list

Validate each phone number in a list

Bind validator to the form

(def form
  (f/constructor validator))

Create a form instance

(def inited-form
  (form {}))

Set the value of the key path in the data atom

(defn setter
  [path data-atom]
  (fn [e]
    (swap! data-atom assoc-in path (.. e -target -value))))

Adds a new entry to the :accounts attribute. After the data is added it will validate the form to potentially remove the validation error for the :accounts attribute.

(defn add-social-media-account
  [form]
  (fn [e]
    (.preventDefault e)
    (let [form-data-atom (f/data form)
          form-data @form-data-atom
          accounts (or (:accounts form-data) [])]
      (swap! form-data-atom assoc :accounts (conj accounts {:username nil :network nil}))
      (f/validate! form true))))

Adds a new entry to the :phone-numbers attribute. After the data is added it will validate the form to potentially remove the validation error for the :phone-numbers attribute.

(defn add-phone-number
  [form]
  (fn [e]
    (.preventDefault e)
    (let [form-data-atom (f/data form)
          form-data @form-data-atom
          accounts (or (:phone-numbers form-data) [])]
      (swap! form-data-atom assoc :phone-numbers (conj accounts nil))
      (f/validate! form true))))

Renders the errors for the given key-path.

(defn render-errors
  [form path]
  (let [errors @(f/errors-for-path form path)]
    (when errors
      [:div.text-danger.errors-wrap 
       [:ul.list-unstyled
        (map (fn [error]
               [:li {:key error} (get-in validations [error :message])]) (:failed errors))]])))

Renders an input field and it's errors.

Validation behaves differently based on the key path error state:

  • If the key path is in the valid state, validation will be triggered on blur
  • If the key path is in the invalid state, validation will be triggered on change
(defn render-input
  ([form path label] (render-input form path label :text))
  ([form path label type]
   (fn [] 
     (let [form-data-atom (f/data form)
           form-data @form-data-atom
           is-valid? @(f/is-valid-path? form path)
           input-setter (setter path form-data-atom)
           on-change-handler (fn [e]
                               (input-setter e)
                               (when (not is-valid?)
                                 (f/validate! form true)))
           errors @(f/errors-for-path form path)]
       [:div.form-group {:class (when (not is-valid?) "has-error")}
        [:label.control-label label]
        [:input.form-control {:type type
                              :value (get-in form-data path)
                              :on-change on-change-handler
                              :on-blur #(f/validate! form true)}]
        [render-errors form path]]))))

Renders the select box for the social network. After the social network is selected it will validate the form to show or remove the error messages.

(defn render-social-media-select
  [form path]
  (fn []
    (let [form-data-atom (f/data form)
          form-data @form-data-atom
          is-valid? @(f/is-valid-path? form path)
          select-setter (setter path form-data-atom)
          set-and-validate (fn [e]
                             (select-setter e)
                             (f/validate! form true))]
      [:div.form-group {:class (when (not is-valid?) "has-error")}
       [:label.control-label "Social Network"]
       [:select.form-control {:on-change set-and-validate 
                              :value (get-in form-data path)}
        [:option {:value ""} "Please select"]
        [:option {:value "facebook"} "Facebook"]
        [:option {:value "twitter"} "Twitter"]
        [:option {:value "instagram"} "Instagram"]]
       [render-errors form path]])))

Renders a list of the social network accounts (nested) form fields.

(defn render-social-media-accounts
  [form]
  (fn []
    (let [form-data-atom (f/data form)
          form-data @form-data-atom
          accounts (:accounts form-data)]
      (when (pos? (count accounts))
        [:div.well.nested-wrap 
         (doall (map-indexed
                 (fn [idx item]
                   [:div.social-network-account-wrap {:key idx}
                    [:h3 (str "Account #" (inc idx))]
                    [render-social-media-select form [:accounts idx :network]]
                    [render-input form [:accounts idx :username] "Username"]]) accounts))]))))

Renders a list of the phone number input fields

(defn render-phone-numbers
  [form]
  (fn []
    (let [form-data-atom (f/data form)
          form-data @form-data-atom
          phone-numbers (:phone-numbers form-data)]
      [:div.nested-wrap 
       (doall (map-indexed
               (fn [idx item]
                 [:div {:key idx}
                  [render-input form [:phone-numbers idx] (str "Phone number #" (inc idx))]]) phone-numbers))])))

Main template, renders the form and all of the fields. When the form is submitted it will validate the whole form which will potentially render the errors.

(defn forms-render
  [form]
  (let [on-submit (fn [e]
                    (.preventDefault e)
                    (f/validate! form))]
    [:div.container>div.row>div.col-xs-12
     [:form {:on-submit on-submit}
      [:h1 "User Info"]
      [render-input form [:username] "Username"]
      [render-input form [:password] "Password" :password]
      [render-input form [:name] "Name"]
      [render-input form [:email] "Email"]
      [:hr]
      [:h2 "Social Network accounts"]
      [:button.btn
       {:on-click (add-social-media-account form)}
       "Add Social Network account"]
      [render-social-media-accounts form]
      [render-errors form :accounts]
      [:hr]
      [:h2 "Phone Numbers"]
      [:button.btn
       {:on-click (add-phone-number form)}
       "Add Phone Number"]
      [render-phone-numbers form]
      [:hr]
      [:button.btn.btn-primary "Submit"]]
     [:hr]
     [:h1 "Form Data:"]
     [:div.form-data-wrapper
      (j/edn->hiccup @(f/data form))]]))
(defn reload []
  (reagent/render [forms-render inited-form]
                  (.getElementById js/document "app")))
(defn ^:export main []
  (reload))
 
(ns keechma-forms-example.core)