├── .idea ├── .name ├── copyright │ └── profiles_settings.xml ├── encodings.xml ├── vcs.xml ├── modules.xml ├── inspectionProfiles │ ├── profiles_settings.xml │ └── Project_Default.xml ├── misc.xml ├── runConfigurations │ └── lein_doo_chrome_test_auto.xml └── compiler.xml ├── .travis.yml ├── src └── cljs │ └── free_form │ ├── extension.cljs │ ├── debug.cljs │ ├── re_frame.cljs │ ├── util.cljs │ ├── core.cljs │ └── bootstrap_3.cljs ├── test └── cljs │ └── free_form │ ├── runner.cljs │ └── core_test.cljs ├── .gitignore ├── project.clj ├── free-form.iml ├── LICENSE └── README.md /.idea/.name: -------------------------------------------------------------------------------- 1 | free-form -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: clojure 2 | script: lein doo phantom test once -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/cljs/free_form/extension.cljs: -------------------------------------------------------------------------------- 1 | ;;;; Copyright © 2017 José Pablo Fernández Silva 2 | 3 | (ns free-form.extension) 4 | 5 | (defmulti extension (fn [extension-name _inner-fn] extension-name)) 6 | 7 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /test/cljs/free_form/runner.cljs: -------------------------------------------------------------------------------- 1 | ;;;; Copyright © 2016-2017 José Pablo Fernández Silva 2 | 3 | (ns free-form.runner 4 | (:require [doo.runner :refer-macros [doo-tests]] 5 | [free-form.core-test])) 6 | 7 | (doo-tests 'free-form.core-test) 8 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /src/cljs/free_form/debug.cljs: -------------------------------------------------------------------------------- 1 | ;;;; Copyright © 2017 José Pablo Fernández Silva 2 | 3 | (ns free-form.debug 4 | (:require [free-form.extension :as extension])) 5 | 6 | (defmethod extension/extension :debug [_extension-name inner-fn] 7 | (fn [html] 8 | (println "Before:" html) 9 | (let [html (inner-fn html)] 10 | (println "After" html) 11 | html))) 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /out 4 | /checkouts 5 | pom.xml 6 | pom.xml.asc 7 | *.jar 8 | *.class 9 | /.lein-* 10 | /.nrepl-port 11 | .hgignore 12 | .hg/ 13 | 14 | # IntelliJ https://github.com/github/gitignore/blob/master/Global/JetBrains.gitignore 15 | .idea/workspace.xml 16 | .idea/tasks.xml 17 | .idea/dictionaries 18 | .idea/libraries 19 | .idea/replstate.xml 20 | *.ipr 21 | *.iws 22 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/runConfigurations/lein_doo_chrome_test_auto.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/cljs/free_form/re_frame.cljs: -------------------------------------------------------------------------------- 1 | ;;;; Copyright © 2015-2017 José Pablo Fernández Silva 2 | 3 | (ns free-form.re-frame 4 | (:require [free-form.core :as core] 5 | [re-frame.core :as re-frame])) 6 | 7 | (defn form [& args] 8 | (let [event (nth args 2) 9 | re-frame-event-generator (fn [keys value] 10 | (let [event-v (cond 11 | (fn? event) (event keys value) 12 | (vector? event) (conj event keys value) 13 | :else [event keys value])] 14 | (re-frame/dispatch event-v))) 15 | args (assoc (vec args) 2 re-frame-event-generator)] 16 | (into [core/form] args))) 17 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/cljs/free_form/util.cljs: -------------------------------------------------------------------------------- 1 | ;;;; Copyright © 2017 José Pablo Fernández Silva 2 | 3 | (ns free-form.util) 4 | 5 | (defn field? [node] 6 | (and (coll? node) (= :free-form/field (first node)))) 7 | 8 | (defn key->keys [m] 9 | (if (contains? m :key) 10 | (if (contains? m :keys) 11 | (throw (js/Error. "key->keys expects a map with :key or :keys, not both")) 12 | (assoc m :keys [(:key m)])) 13 | m)) 14 | 15 | (def attributes-index 16 | "The second element in structure that represents an input is the attributes, as in :type, :key, etc." 17 | 1) 18 | 19 | ; Not needed yet, but might be needed in the future 20 | #_(defn- remove-free-form-attribute [node attr-location attr-name] 21 | (let [node (update-in node [attributes-index attr-location] dissoc attr-name)] 22 | (if (empty? (get-in node [attributes-index attr-location])) 23 | (update-in node [attributes-index] dissoc attr-location) 24 | node))) 25 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | ;;;; Copyright © 2015-2017 José Pablo Fernández Silva 2 | 3 | (defproject com.pupeno/free-form "0.6.0" 4 | :description "Library for building forms with Reagent or Re-frame." 5 | :url "https://github.com/pupeno/free-form" 6 | :license {:name "Eclipse Public License" 7 | :url "http://www.eclipse.org/legal/epl-v10.html"} 8 | 9 | :lein-release {:deploy-via :clojars} 10 | :signing {:gpg-key "71E6E789"} 11 | :scm {:name "git" 12 | :url "https://github.com/pupeno/free-form"} 13 | 14 | :dependencies [[org.clojure/clojurescript "1.9.293" :scope "provided"] 15 | [org.clojure/clojure "1.8.0" :scope "provided"] 16 | [reagent "0.6.0" :scope "provided"] 17 | [re-frame "0.8.0" :scope "provided"] 18 | [doo "0.1.7" :scope "provided"]] 19 | :plugins [[lein-cljsbuild "1.1.4"] 20 | [lein-doo "0.1.7"]] 21 | 22 | :source-paths ["src/cljs"] 23 | :cljsbuild {:builds {:test {:source-paths ["src/cljs" "test/cljs"] 24 | :compiler {:main free-form.runner 25 | :output-to "out/free_form.js" 26 | :optimizations :none}}}} 27 | 28 | :doo {:build "test"}) 29 | -------------------------------------------------------------------------------- /free-form.iml: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /src/cljs/free_form/core.cljs: -------------------------------------------------------------------------------- 1 | ;;;; Copyright © 2015-2017 José Pablo Fernández Silva 2 | 3 | (ns free-form.core 4 | (:require clojure.string 5 | [clojure.walk :refer [postwalk prewalk]] 6 | [free-form.extension :as extension] 7 | [free-form.util :refer [field? key->keys attributes-index]])) 8 | 9 | (defn- extract-attributes [node key] 10 | (let [attributes (get node attributes-index) 11 | re-attributes (key attributes) 12 | attributes (dissoc attributes key) 13 | keys (or (:keys re-attributes) [(:key re-attributes)])] 14 | [attributes re-attributes keys])) 15 | 16 | (defn- input? [node] 17 | (and (coll? node) 18 | (contains? (second node) :free-form/input))) 19 | 20 | (defn- js-event-value [event] 21 | (let [target (.-target event)] 22 | (case (.-type target) 23 | "checkbox" (.-checked target) 24 | (.-value target)))) 25 | 26 | (defn- extract-event-value [event] 27 | (if (or (boolean? event) 28 | (string? event)) 29 | event ; React-toolbox generates events that already contain a stracted string of the value as the first paramenter 30 | (js-event-value event))) ; for all other cases, we extract it ourselves. 31 | 32 | (defn- first-non-nil [& coll] 33 | (first (filter (complement nil?) coll))) 34 | 35 | (defn- bind-input [values errors on-change node] 36 | (if (not (input? node)) 37 | node 38 | (let [[attributes free-form-attributes keys] (extract-attributes node :free-form/input) 39 | {:keys [value-on error-on extra-error-keys]} free-form-attributes 40 | on-change-fn #(on-change keys (extract-event-value %1)) 41 | value-on (or value-on (case (:type attributes) 42 | (:checkbox :radio) :default-checked 43 | :value)) 44 | value (case (:type attributes) 45 | :checkbox (= true (get-in values keys)) 46 | :radio (= (:value attributes) (get-in values keys)) 47 | (first-non-nil (get-in values keys) (:blank-value free-form-attributes) "")) 48 | input-errors (get-in errors keys)] 49 | (assoc node attributes-index 50 | (cond-> attributes 51 | true (assoc :on-change on-change-fn) 52 | true (assoc value-on value) 53 | (and error-on input-errors) (assoc error-on (clojure.string/join " " input-errors)) 54 | (and extra-error-keys (some #(get-in errors %) extra-error-keys)) (assoc error-on " ")))))) 55 | 56 | (defn- error-class? 57 | "Tests whether the node should be marked with an error class should the field have an associated error." 58 | [node] 59 | (and (coll? node) 60 | (contains? (second node) :free-form/error-class))) 61 | 62 | (defn- bind-error-class [errors node] 63 | (if (not (error-class? node)) 64 | node 65 | (let [[attributes re-attributes keys] (extract-attributes node :free-form/error-class)] 66 | (assoc node attributes-index 67 | (if (not-any? #(get-in errors %) (conj (:extra-keys re-attributes) keys)) 68 | attributes 69 | (update attributes :class #(str (or (:error re-attributes) "error") %))))))) 70 | 71 | (defn- error-messages? 72 | [node] 73 | (and (coll? node) 74 | (contains? (second node) :free-form/error-message))) 75 | 76 | (defn- bind-error-messages [errors node] 77 | (if (not (error-messages? node)) 78 | node 79 | (let [[attributes _ keys] (extract-attributes node :free-form/error-message)] 80 | (if-let [errors (get-in errors keys)] 81 | (vec (concat 82 | (drop-last (assoc node attributes-index attributes)) 83 | (map #(conj (get node 2) %) errors))) 84 | nil)))) 85 | 86 | (defn- warn-of-leftovers [node] 87 | (let [attrs (get node attributes-index)] 88 | (when (and (map? attrs) 89 | (some #(= "free-form" (namespace %)) (keys attrs))) 90 | (js/console.error "There are free-form-looking leftovers on" (pr-str node)))) 91 | node) 92 | 93 | (defn form 94 | ([values errors on-change html] 95 | (form values errors on-change [] html)) 96 | ([values errors on-change extensions html] 97 | (let [errors (or errors {}) 98 | extensions (if (sequential? extensions) extensions [extensions]) 99 | inner-fn (fn [html] 100 | (->> html 101 | (postwalk #(bind-input values errors on-change %)) 102 | (postwalk #(bind-error-class errors %)) 103 | (postwalk #(bind-error-messages errors %))))] 104 | (postwalk #(warn-of-leftovers %) 105 | ((reduce #(extension/extension %2 %1) inner-fn extensions) html))))) 106 | -------------------------------------------------------------------------------- /src/cljs/free_form/bootstrap_3.cljs: -------------------------------------------------------------------------------- 1 | ;;;; Copyright © 2017 José Pablo Fernández Silva 2 | 3 | (ns free-form.bootstrap-3 4 | (:require [clojure.walk :refer [postwalk prewalk]] 5 | [clojure.string :as s] 6 | [free-form.util :refer [field? key->keys attributes-index]] 7 | [free-form.extension :as extension])) 8 | 9 | (defn- expand-bootstrap-3-input [id keys type placeholder options] 10 | (case type 11 | :select [:select.form-control {:free-form/input {:keys keys} 12 | :type type 13 | :id id 14 | :placeholder placeholder} 15 | (letfn [(generate-option [[value name]] 16 | (if (sequential? name) 17 | ^{:key value} [:optgroup {:label value} 18 | (map generate-option (partition 2 name))] 19 | ^{:key value} [:option {:value value} name]))] 20 | (map generate-option (partition 2 options)))] 21 | :textarea [:textarea.form-control {:free-form/input {:keys keys} 22 | :type type 23 | :id id}] 24 | :checkbox [:input {:free-form/input {:keys keys} 25 | :type type 26 | :id id}] 27 | :radio (map (fn [[value name]] 28 | ^{:name name} 29 | [:input {:free-form/input {:keys keys} 30 | :type type 31 | :name id 32 | :value value}]) 33 | (partition 2 options)) 34 | [:input.form-control {:free-form/input {:keys keys} 35 | :type type 36 | :id id 37 | :placeholder placeholder}])) 38 | 39 | (defn- expand-bootstrap-3-fields [node] 40 | (if (field? node) 41 | (let [{:keys [type keys extra-validation-error-keys label placeholder options]} (key->keys (second node)) 42 | id (s/join "-" (map name keys))] 43 | (case type 44 | :checkbox [:div.checkbox {:free-form/error-class {:keys keys 45 | :extra-keys extra-validation-error-keys 46 | :error "has-error"}} 47 | [:label (expand-bootstrap-3-input id keys type placeholder options) label] 48 | [:div.text-danger {:free-form/error-message {:keys keys}} [:p]]] 49 | :radio [:div {:free-form/error-class {:keys keys 50 | :extra-keys extra-validation-error-keys 51 | :error "has-error"}} 52 | [:label label] 53 | (map (fn [input] 54 | ^{:key (str id "-" (get-in input [1 :value]))} 55 | [:div.radio 56 | [:label input (:name (meta input))]]) 57 | (expand-bootstrap-3-input id keys type placeholder options)) 58 | [:div.text-danger {:free-form/error-message {:keys keys}} [:p]]] 59 | [:div.form-group {:free-form/error-class {:keys keys :extra-keys extra-validation-error-keys :error "has-error"}} 60 | [:label.control-label {:for id} label] 61 | (expand-bootstrap-3-input id keys type placeholder options) 62 | [:div.text-danger {:free-form/error-message {:keys keys}} [:p]]])) 63 | node)) 64 | 65 | (defn- expand-bootstrap-3-horizontal-fields [node] 66 | (if (field? node) 67 | (let [{:keys [type keys extra-validation-error-keys label placeholder options]} (key->keys (second node)) 68 | id (s/join "-" (map name keys))] 69 | (case type 70 | :checkbox [:div.form-group {:free-form/error-class {:keys keys 71 | :extra-keys extra-validation-error-keys 72 | :error "has-error"}} 73 | [:div.col-sm-offset-2.col-sm-10 74 | [:div.checkbox 75 | [:label (expand-bootstrap-3-input id keys type placeholder options) label]] 76 | [:div.text-danger {:free-form/error-message {:keys keys}} [:p]]]] 77 | :radio [:div.form-group {:free-form/error-class {:keys keys 78 | :extra-keys extra-validation-error-keys 79 | :error "has-error"}} 80 | [:label.col-sm-2.control-label label] 81 | (-> [:div.col-sm-10] 82 | (into (map (fn [input] 83 | ^{:key (str id "-" (get-in input [1 :value]))} 84 | [:div.radio 85 | [:label input (:name (meta input))]]) 86 | (expand-bootstrap-3-input id keys type placeholder options))) 87 | (conj [:div.text-danger {:free-form/error-message {:keys keys}} [:p]]))] 88 | [:div.form-group {:free-form/error-class {:keys keys 89 | :extra-keys extra-validation-error-keys 90 | :error "has-error"}} 91 | [:label.col-sm-2.control-label {:for id} label] 92 | [:div.col-sm-10 (expand-bootstrap-3-input id keys type placeholder options) 93 | [:div.text-danger {:free-form/error-message {:keys keys}} [:p]]]])) 94 | node)) 95 | 96 | (defn- expand-bootstrap-3-inline-fields [node] 97 | (if (field? node) 98 | (let [{:keys [type keys extra-validation-error-keys label placeholder options]} (key->keys (second node)) 99 | id (s/join "-" (map name keys))] 100 | (case type 101 | :checkbox [:div.form-group 102 | [:label.checkbox-inline {:free-form/error-class {:keys keys 103 | :extra-keys extra-validation-error-keys 104 | :error "has-error"}} 105 | (expand-bootstrap-3-input id keys type placeholder options) label] 106 | [:div.text-danger {:free-form/error-message {:keys keys}} [:p]]] 107 | :radio [:div.form-group {:free-form/error-class {:keys keys 108 | :extra-keys extra-validation-error-keys 109 | :error "has-error"}} 110 | (when label [:label label]) 111 | (map (fn [input] 112 | ^{:key (str id "-" (get-in input [1 :value]))} 113 | [:div.radio-inline 114 | [:label input (:name (meta input))]]) 115 | (expand-bootstrap-3-input id keys type placeholder options)) 116 | [:div.text-danger {:free-form/error-message {:keys keys}} [:p]]] 117 | [:div.form-group {:free-form/error-class {:keys keys 118 | :extra-keys extra-validation-error-keys 119 | :error "has-error"}} 120 | [:label.control-label {:for id} label] 121 | " " 122 | (expand-bootstrap-3-input id keys type placeholder options) 123 | " " 124 | [:div.text-danger {:free-form/error-message {:keys keys}} [:p]]])) 125 | node)) 126 | 127 | (defn- bootstrap-3-form-horizontal? [node] 128 | (and (coll? node) 129 | (= :form.form-horizontal (first node)))) 130 | 131 | (defn- bootstrap-3-form-inline? [node] 132 | (and (coll? node) 133 | (= :form.form-inline (first node)))) 134 | 135 | (defn- expand-bootstrap-3-form [node] 136 | (cond (bootstrap-3-form-horizontal? node) (postwalk expand-bootstrap-3-horizontal-fields node) 137 | (bootstrap-3-form-inline? node) (postwalk expand-bootstrap-3-inline-fields node) 138 | :else (postwalk expand-bootstrap-3-fields node))) 139 | 140 | (defmethod extension/extension :bootstrap-3 [_extension-name inner-fn] 141 | (fn [html] 142 | (inner-fn (prewalk expand-bootstrap-3-form html)))) 143 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC 2 | LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM 3 | CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 4 | 5 | 1. DEFINITIONS 6 | 7 | "Contribution" means: 8 | 9 | a) in the case of the initial Contributor, the initial code and 10 | documentation distributed under this Agreement, and 11 | 12 | b) in the case of each subsequent Contributor: 13 | 14 | i) changes to the Program, and 15 | 16 | ii) additions to the Program; 17 | 18 | where such changes and/or additions to the Program originate from and are 19 | distributed by that particular Contributor. A Contribution 'originates' from 20 | a Contributor if it was added to the Program by such Contributor itself or 21 | anyone acting on such Contributor's behalf. Contributions do not include 22 | additions to the Program which: (i) are separate modules of software 23 | distributed in conjunction with the Program under their own license 24 | agreement, and (ii) are not derivative works of the Program. 25 | 26 | "Contributor" means any person or entity that distributes the Program. 27 | 28 | "Licensed Patents" mean patent claims licensable by a Contributor which are 29 | necessarily infringed by the use or sale of its Contribution alone or when 30 | combined with the Program. 31 | 32 | "Program" means the Contributions distributed in accordance with this 33 | Agreement. 34 | 35 | "Recipient" means anyone who receives the Program under this Agreement, 36 | including all Contributors. 37 | 38 | 2. GRANT OF RIGHTS 39 | 40 | a) Subject to the terms of this Agreement, each Contributor hereby grants 41 | Recipient a non-exclusive, worldwide, royalty-free copyright license to 42 | reproduce, prepare derivative works of, publicly display, publicly perform, 43 | distribute and sublicense the Contribution of such Contributor, if any, and 44 | such derivative works, in source code and object code form. 45 | 46 | b) Subject to the terms of this Agreement, each Contributor hereby grants 47 | Recipient a non-exclusive, worldwide, royalty-free patent license under 48 | Licensed Patents to make, use, sell, offer to sell, import and otherwise 49 | transfer the Contribution of such Contributor, if any, in source code and 50 | object code form. This patent license shall apply to the combination of the 51 | Contribution and the Program if, at the time the Contribution is added by the 52 | Contributor, such addition of the Contribution causes such combination to be 53 | covered by the Licensed Patents. The patent license shall not apply to any 54 | other combinations which include the Contribution. No hardware per se is 55 | licensed hereunder. 56 | 57 | c) Recipient understands that although each Contributor grants the licenses 58 | to its Contributions set forth herein, no assurances are provided by any 59 | Contributor that the Program does not infringe the patent or other 60 | intellectual property rights of any other entity. Each Contributor disclaims 61 | any liability to Recipient for claims brought by any other entity based on 62 | infringement of intellectual property rights or otherwise. As a condition to 63 | exercising the rights and licenses granted hereunder, each Recipient hereby 64 | assumes sole responsibility to secure any other intellectual property rights 65 | needed, if any. For example, if a third party patent license is required to 66 | allow Recipient to distribute the Program, it is Recipient's responsibility 67 | to acquire that license before distributing the Program. 68 | 69 | d) Each Contributor represents that to its knowledge it has sufficient 70 | copyright rights in its Contribution, if any, to grant the copyright license 71 | set forth in this Agreement. 72 | 73 | 3. REQUIREMENTS 74 | 75 | A Contributor may choose to distribute the Program in object code form under 76 | its own license agreement, provided that: 77 | 78 | a) it complies with the terms and conditions of this Agreement; and 79 | 80 | b) its license agreement: 81 | 82 | i) effectively disclaims on behalf of all Contributors all warranties and 83 | conditions, express and implied, including warranties or conditions of title 84 | and non-infringement, and implied warranties or conditions of merchantability 85 | and fitness for a particular purpose; 86 | 87 | ii) effectively excludes on behalf of all Contributors all liability for 88 | damages, including direct, indirect, special, incidental and consequential 89 | damages, such as lost profits; 90 | 91 | iii) states that any provisions which differ from this Agreement are offered 92 | by that Contributor alone and not by any other party; and 93 | 94 | iv) states that source code for the Program is available from such 95 | Contributor, and informs licensees how to obtain it in a reasonable manner on 96 | or through a medium customarily used for software exchange. 97 | 98 | When the Program is made available in source code form: 99 | 100 | a) it must be made available under this Agreement; and 101 | 102 | b) a copy of this Agreement must be included with each copy of the Program. 103 | 104 | Contributors may not remove or alter any copyright notices contained within 105 | the Program. 106 | 107 | Each Contributor must identify itself as the originator of its Contribution, 108 | if any, in a manner that reasonably allows subsequent Recipients to identify 109 | the originator of the Contribution. 110 | 111 | 4. COMMERCIAL DISTRIBUTION 112 | 113 | Commercial distributors of software may accept certain responsibilities with 114 | respect to end users, business partners and the like. While this license is 115 | intended to facilitate the commercial use of the Program, the Contributor who 116 | includes the Program in a commercial product offering should do so in a 117 | manner which does not create potential liability for other Contributors. 118 | Therefore, if a Contributor includes the Program in a commercial product 119 | offering, such Contributor ("Commercial Contributor") hereby agrees to defend 120 | and indemnify every other Contributor ("Indemnified Contributor") against any 121 | losses, damages and costs (collectively "Losses") arising from claims, 122 | lawsuits and other legal actions brought by a third party against the 123 | Indemnified Contributor to the extent caused by the acts or omissions of such 124 | Commercial Contributor in connection with its distribution of the Program in 125 | a commercial product offering. The obligations in this section do not apply 126 | to any claims or Losses relating to any actual or alleged intellectual 127 | property infringement. In order to qualify, an Indemnified Contributor must: 128 | a) promptly notify the Commercial Contributor in writing of such claim, and 129 | b) allow the Commercial Contributor tocontrol, and cooperate with the 130 | Commercial Contributor in, the defense and any related settlement 131 | negotiations. The Indemnified Contributor may participate in any such claim 132 | at its own expense. 133 | 134 | For example, a Contributor might include the Program in a commercial product 135 | offering, Product X. That Contributor is then a Commercial Contributor. If 136 | that Commercial Contributor then makes performance claims, or offers 137 | warranties related to Product X, those performance claims and warranties are 138 | such Commercial Contributor's responsibility alone. Under this section, the 139 | Commercial Contributor would have to defend claims against the other 140 | Contributors related to those performance claims and warranties, and if a 141 | court requires any other Contributor to pay any damages as a result, the 142 | Commercial Contributor must pay those damages. 143 | 144 | 5. NO WARRANTY 145 | 146 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON 147 | AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER 148 | EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR 149 | CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A 150 | PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the 151 | appropriateness of using and distributing the Program and assumes all risks 152 | associated with its exercise of rights under this Agreement , including but 153 | not limited to the risks and costs of program errors, compliance with 154 | applicable laws, damage to or loss of data, programs or equipment, and 155 | unavailability or interruption of operations. 156 | 157 | 6. DISCLAIMER OF LIABILITY 158 | 159 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY 160 | CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, 161 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION 162 | LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 163 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 164 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE 165 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY 166 | OF SUCH DAMAGES. 167 | 168 | 7. GENERAL 169 | 170 | If any provision of this Agreement is invalid or unenforceable under 171 | applicable law, it shall not affect the validity or enforceability of the 172 | remainder of the terms of this Agreement, and without further action by the 173 | parties hereto, such provision shall be reformed to the minimum extent 174 | necessary to make such provision valid and enforceable. 175 | 176 | If Recipient institutes patent litigation against any entity (including a 177 | cross-claim or counterclaim in a lawsuit) alleging that the Program itself 178 | (excluding combinations of the Program with other software or hardware) 179 | infringes such Recipient's patent(s), then such Recipient's rights granted 180 | under Section 2(b) shall terminate as of the date such litigation is filed. 181 | 182 | All Recipient's rights under this Agreement shall terminate if it fails to 183 | comply with any of the material terms or conditions of this Agreement and 184 | does not cure such failure in a reasonable period of time after becoming 185 | aware of such noncompliance. If all Recipient's rights under this Agreement 186 | terminate, Recipient agrees to cease use and distribution of the Program as 187 | soon as reasonably practicable. However, Recipient's obligations under this 188 | Agreement and any licenses granted by Recipient relating to the Program shall 189 | continue and survive. 190 | 191 | Everyone is permitted to copy and distribute copies of this Agreement, but in 192 | order to avoid inconsistency the Agreement is copyrighted and may only be 193 | modified in the following manner. The Agreement Steward reserves the right to 194 | publish new versions (including revisions) of this Agreement from time to 195 | time. No one other than the Agreement Steward has the right to modify this 196 | Agreement. The Eclipse Foundation is the initial Agreement Steward. The 197 | Eclipse Foundation may assign the responsibility to serve as the Agreement 198 | Steward to a suitable separate entity. Each new version of the Agreement will 199 | be given a distinguishing version number. The Program (including 200 | Contributions) may always be distributed subject to the version of the 201 | Agreement under which it was received. In addition, after a new version of 202 | the Agreement is published, Contributor may elect to distribute the Program 203 | (including its Contributions) under the new version. Except as expressly 204 | stated in Sections 2(a) and 2(b) above, Recipient receives no rights or 205 | licenses to the intellectual property of any Contributor under this 206 | Agreement, whether expressly, by implication, estoppel or otherwise. All 207 | rights in the Program not expressly granted under this Agreement are 208 | reserved. 209 | 210 | This Agreement is governed by the laws of the State of New York and the 211 | intellectual property laws of the United States of America. No party to this 212 | Agreement will bring a legal action under this Agreement more than one year 213 | after the cause of action arose. Each party waives its rights to a jury trial 214 | in any resulting litigation. 215 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Free-form 2 | 3 | [![Code at GitHub](https://img.shields.io/badge/code-github-green.svg)](https://github.com/pupeno/free-form) 4 | [![Clojars](https://img.shields.io/clojars/v/com.pupeno/free-form.svg)](https://clojars.org/com.pupeno/free-form) 5 | [![Build Status](https://travis-ci.org/pupeno/free-form.svg?branch=master)](https://travis-ci.org/pupeno/free-form) 6 | 7 | A ClojureScript library to help building web forms with [Reagent](https://reagent-project.github.io/) and optionally 8 | [re-frame](https://github.com/Day8/re-frame) (others are welcome too). The guiding principles behind [Free-form](https://github.com/pupeno/free-form) 9 | is that you are in control of both you data workflow as well as your markup. You can see the library in action in the 10 | [Free-form Examples app](http://free-form-examples.pupeno.com). 11 | 12 | Free-form doesn't force any markup on you and thus creating your own is a first-class approach. To avoid code 13 | duplication there's an extension system that allows you to write forms in a very succinct way. A Bootstrap 3 extension 14 | comes with Free form but adding more is not hard. One of the advantages of these mechanism is that when you have a 15 | couple of fields that behave differently and need their own markup, you can still use a first class API and enjoy the 16 | advantage of value handling, validation errors and everything Free form has to offer. 17 | 18 | Free-form doesn't handle state for you. You need to decide how to handle state. Free-form comes with a re-frame module 19 | that helps you plug your form into the re-frame state. If you use the Reagent core you can handle state anyway you want. 20 | Other modules could be created to handle state in different ways, for example, one could be created to have a 21 | [private ratom the way Reagent-forms do it](https://github.com/pupeno/free-form/issues/1). 22 | 23 | Free-form supports but doesn't provide validation. It works very well, for example, with [Validateur](http://clojurevalidations.info/). 24 | Nothing special has been done for this, so it should be flexible for any library. If it isn't, please, 25 | [submit a bug report](https://github.com/pupeno/free-form/issues/new). 26 | 27 | The way this library works is that you write (or generate) the HTML template the way you normally do with Reagent, for 28 | example: 29 | 30 | ```clojure 31 | [:input {:type :email 32 | :id :email 33 | :placeholder "sam@example.com"}] 34 | ``` 35 | 36 | which then you pepper with special keywords to trigger the activation of inputs, labels, validation, etc. For example, 37 | to make this input to connect to the email we would change it to: 38 | 39 | ```clojure 40 | [:input {:free-form/input {:key :email} 41 | :type :email 42 | :id :email 43 | :placeholder "sam@example.com"}] 44 | ``` 45 | 46 | [Reagent-forms](https://github.com/reagent-project/reagent-forms) was a big inspiration but the way it handles state was 47 | not ideal in a re-frame scenario. 48 | 49 | ## Usage 50 | 51 | First, you have to include Free-form in your project: 52 | 53 | [![Clojars Project](http://clojars.org/com.pupeno/free-form/latest-version.svg)](http://clojars.org/com.pupeno/free-form) 54 | 55 | To activate a form you call ```free-form.core/form``` passing the set of values to display when the form is shown for 56 | the first time, the set of errors to display, a callback function to receive changes to the state and the form itself. 57 | For example: 58 | 59 | ```clojure 60 | [free-form.core/form {:email "pupeno@example.com"} 61 | {:email ["Email addresses can't end in @example.com"]} 62 | (fn [keys value] (println "Value for" keys "changed to" value)) 63 | [...]] 64 | ``` 65 | 66 | Notice that it's using square brackets because it's a Reagent component. You are likely to pass the contents of ratoms 67 | so that the form will be connected to live data, like: 68 | 69 | ```clojure 70 | [free-form.core/form @values @errors save-state 71 | [...]] 72 | ``` 73 | 74 | The form is just your traditional Reagent template: 75 | 76 | ```clojure 77 | [free-form.core/form @values @errors save-state 78 | [:label {:for :email} "Email"] 79 | [:input.form-control {:free-form/input {:key :email} 80 | :free-form/error-class {:key :text :error "error"} 81 | :type :email 82 | :id :email}] 83 | [:div.errors {:free-form/error-message {:key :email}} [:p.error]]] 84 | ``` 85 | 86 | There are three special keywords added: 87 | * ```:free-form/input``` marks the element as being an input and the passed key is to be used to connect to the value. As an alternative, you can pass a set of keys, as in: ```{:keys [:user :email]}```, as you do with the function ```get-in```. 88 | * ```:free-form/error-class``` will add a class if there's a validation error for the field. As with the previous one, ```:key``` or ```:keys``` marks the field, and ```:error``` the class to be added in case of error. 89 | * ```:free-form/error-message``` adds error messages. If there are no error messages, the surrounding element, in this case ```:div.errors``` will not be output at all. The field to be read from the map of errors is specified by ```:key``` or ```:keys```. Lastly, the element inside this element will be used to output each of the error messages, so this might end up looking like: ```[:div.error [:p.error "Password is too short"] [:p.error "Password must contain a symbol"]]``` 90 | 91 | ### re-frame 92 | 93 | When using Free-form with re-frame, the form is built in exactly the same way, but instead of having to code your own 94 | state management function, you can pass the name of the event to be triggered: 95 | 96 | ```clojure 97 | [free-form.re-frame/form @values @errors :update-state 98 | [...]] 99 | ``` 100 | 101 | And the library will dispatch ```[:update-state keys new-value]```. If you need to pass extra arguments to the handler, 102 | specify it as a vector. 103 | 104 | ```clojure 105 | [free-form.re-frame/form @values @errors [:update :user] 106 | [...]] 107 | ``` 108 | 109 | If you need to generate more involved events to 110 | dispatch, you can pass a function that will get the keys and the new value and generate the event to be dispatched. For 111 | example: 112 | 113 | ```clojure 114 | [free-form.re-frame/form @values @errors (fn [keys new-value] [:update :user keys new-value]) 115 | [...]] 116 | ``` 117 | 118 | ### Bootstrap 3 119 | 120 | You can manually generate Bootstrap 3 forms by using code such as: 121 | 122 | ```clojure 123 | [free-form.core/form @values @errors save-state 124 | [:form.form-horizontal 125 | [:div.form-group {:free-form/error-class {:key :email :error "has-error"}} 126 | [:label.col-sm-2.control-label {:for :email} "Email"] 127 | [:div.col-sm-10 [:input.form-control {:free-form/input {:key :email} 128 | :type :email 129 | :id :email}] 130 | [:div.text-danger {:free-form/error-message {:key :email}} [:p]]]]]] 131 | ```` 132 | 133 | but since that pattern is so common, it is now supported by an extension: 134 | 135 | ```clojure 136 | (ns whatever 137 | (:require [free-form.core :as free-form] 138 | free-form.bootstrap-3)) 139 | 140 | [free-form/form @values @errors save-state :bootstrap-3 141 | [:form.form-horizontal 142 | [:free-form/field {:type :email 143 | :key :email 144 | :label "Email"}]]] 145 | ```` 146 | 147 | You need to require ```free-form.bootstrap-3``` for the extension to be available. The extra argument, 148 | ```:bootstrap-3``` is what triggers Bootstrap 3 generation and Free-form will automatically detect whether it's a 149 | [standard](http://free-form-examples.pupeno.com/reagent/bootstrap-3), [horizontal](http://free-form-examples.pupeno.com/reagent/bootstrap-3-horizontal) 150 | or [inline](http://free-form-examples.pupeno.com/reagent/bootstrap-3-inline) form. 151 | 152 | ### Debugging 153 | 154 | The debug extension just prints the form before and after any other processing happens. 155 | 156 | ```clojure 157 | (ns whatever 158 | (:require [free-form.core :as free-form] 159 | free-form.debug)) 160 | ``` 161 | 162 | ### Writing your own extensions 163 | 164 | There's a fourth optional argument to specify one or more extensions to be applied to the form. For example, with only 165 | one extension called bootstrap-3: 166 | 167 | ```clojure 168 | [free-form.core/form @values @errors save-state :bootstrap-3 169 | [...]] 170 | ```` 171 | 172 | or with multiple: 173 | 174 | ```clojure 175 | [free-form.core/form @values @errors save-state [:bootstrap-3 :debug] 176 | [...]] 177 | ```` 178 | 179 | Extensions essentially wrap the form and thus, order is important and they can be provided more than once. For example: 180 | 181 | ```clojure 182 | [free-form.core/form @values @errors save-state [:debug :bootstrap-3 :debug] 183 | [...]] 184 | ```` 185 | 186 | would help you see what the Bootstrap 3 extension is doing. 187 | 188 | Extensions are implemented by adding a method to the multi-method free-form.extension/extension. This method will get 189 | the name of the extension and the function that process the form. This function gets the unprocessed html markup and 190 | returns the processed html structure. The extension should return a function that does essentially the same plus 191 | whatever the extension wants to do. This is a system similar to middlewares found in many libraries. For example: 192 | 193 | ```clojure 194 | (defmethod free-form.extension/extension :extension-name [_extension-name inner-fn] 195 | (fn [html] 196 | (do-something-else (inner-fn (do-something html))))) 197 | ``` 198 | 199 | do-something would pre-process the raw structure and do-something-else would post-process the structure after all inner 200 | extensions and the main inner function have been called. 201 | 202 | See the [debug](https://github.com/pupeno/free-form/blob/master/src/cljs/free_form/debug.cljs) and the 203 | [Bootstrap 3 extension](https://github.com/pupeno/free-form/blob/master/src/cljs/free_form/bootsrap_3.cljs)s for 204 | examples. 205 | 206 | ## Users 207 | 208 | This is a completely incomplete list of people/projects using Free-form: 209 | 210 | - [Dashman](https://dashman.tech) 211 | - [Wieck](https://wieck.com) 212 | - [You?](mailto:pupeno@pupeno.com) 213 | 214 | ## Changelog 215 | 216 | ### v0.6.0 - 2017-08-03 217 | - Add support for checkbox and radio form elements, courtesy of [Scott Bauer](https://github.com/Bauerpauer): https://github.com/pupeno/free-form/pull/26 218 | - Add support for checkbox and radio buttons in the Bootstrap 3 extension. 219 | 220 | ### v0.5.0 - 2017-01-07 221 | - Extension system. 222 | - Bootstrap 3 provided as an extension. 223 | - Debug extension. 224 | 225 | ### v0.4.2 - 2016-11-12 226 | - Make all inputs controlled so changes can come from within our from outside. 227 | 228 | ### v0.4.1 - 2016-10-25 229 | - Added the sources directory to the project.clj so that the library is correctly packaged. 230 | 231 | ### v0.4.0 - 2016-10-24 232 | - Tested Free-form with re-frame 0.8.0 and Reagent 0.6.0. 233 | - Allow marking a field as invalid when another one is invalid with :extra-keys. 234 | - Added error messages to Bootstrap inline and horizontal forms. 235 | - Correctly specify dependencies on Clojure and ClojureScript to avoid fixing to a single version. 236 | - When there's no validation error, don't return the form template (invalid HTML), return nil instead. 237 | - After a form with Bootstrap has been processed, remove the option to trigger that processing (it's invalid HTML). 238 | - Show a JavaScript console error if there are any Free-form leftovers after all processing is done. 239 | 240 | ### v0.3.0 - 2016-08-16 241 | - Changed namespace from com.carouselapps to com.pupeno 242 | - Implemented selects. 243 | - Implemented text areas. 244 | 245 | ### v0.2.1 - 2016-08-22 246 | - Changed the metadata of the library to point to the new namespace. 247 | 248 | ### v0.2.0 - 2015-12-14 249 | - Started Bootstrap 3 support. 250 | - Change API from ```:free-form/field``` to ```:free-form/input```. 251 | - Created example app to help test, exercise and develop the library: http://free-form-examples.pupeno.com 252 | 253 | ### v0.1.1 - 2015-10-15 254 | - Fixed a bug when dealing with errors. 255 | 256 | ### v0.1.0 - 2015-10-11 257 | - Initial version extracted from [Ninja Tools](http://tools.screensaver.ninja). 258 | 259 | ## License 260 | 261 | Copyright © 2015-2017 José Pablo Fernández Silva 262 | 263 | Distributed under the Eclipse Public License either version 1.0 or (at your option) any later version. 264 | -------------------------------------------------------------------------------- /test/cljs/free_form/core_test.cljs: -------------------------------------------------------------------------------- 1 | ;;;; Copyright © 2015-2017 José Pablo Fernández Silva 2 | 3 | (ns free-form.core-test 4 | (:require [clojure.test :refer [deftest testing is]] 5 | [clojure.walk :refer [prewalk]] 6 | [free-form.core :as free-form])) 7 | 8 | (defn- hide-on-change [form] 9 | (prewalk (fn [node] (if (contains? node :on-change) 10 | (assoc node :on-change :was-function) 11 | node)) 12 | form)) 13 | 14 | (deftest a-test 15 | (let [plain-reagent-form-template [:form {:noValidate true} 16 | [:div.errors {:free-form/error-message {:key :-general}} [:p.error]] 17 | [:div.plain-field {:free-form/error-class {:key :text :error "validation-errors"}} 18 | [:label {:for :text} "Text"] 19 | [:input {:free-form/input {:key :text} 20 | :type :text 21 | :id :text 22 | :placeholder "placeholder"}] 23 | [:div.errors {:free-form/error-message {:key :text}} [:p.error]]] 24 | [:div.plain-field {:free-form/error-class {:key :email :error "validation-errors"}} 25 | [:label {:for :email} "Email"] 26 | [:input {:free-form/input {:key :email} 27 | :type :email 28 | :id :email 29 | :placeholder "placeholder@example.com"}] 30 | [:div.errors {:free-form/error-message {:key :email}} [:p.error]]] 31 | [:div.plain-field {:free-form/error-class {:key :password :error "validation-errors"}} 32 | [:label {:for :password} "Password"] 33 | [:input {:free-form/input {:key :password} 34 | :type :password 35 | :id :password}] 36 | [:div.errors {:free-form/error-message {:key :password}} [:p.error]]] 37 | [:div.plain-field {:free-form/error-class {:key :select :error "validation-errors"}} 38 | [:label {:for :select} "Select"] 39 | [:select {:free-form/input {:key :select} 40 | :type :select 41 | :id :select} 42 | [:option] 43 | [:option {:value :dog} "Dog"] 44 | [:option {:value :cat} "Cat"] 45 | [:option {:value :squirrel} "Squirrel"] 46 | [:option {:value :giraffe} "Giraffe"]] 47 | [:div.errors {:free-form/error-message {:key :select}} [:p.error]]] 48 | [:div.plain-field {:free-form/error-class {:key :select-with-group :error "validation-errors"}} 49 | [:label {:for :select} "Select with groups"] 50 | [:select {:free-form/input {:key :select-with-group} 51 | :type :select 52 | :id :select-with-group} 53 | [:option] 54 | [:optgroup {:label "Numbers"} 55 | [:option {:value :one} "One"] 56 | [:option {:value :two} "Two"] 57 | [:option {:value :three} "Three"] 58 | [:option {:value :four} "Four"]] 59 | [:optgroup {:label "Leters"} 60 | [:option {:value :a} "A"] 61 | [:option {:value :b} "B"] 62 | [:option {:value :c} "C"] 63 | [:option {:value :d} "D"]]] 64 | [:div.errors {:free-form/error-message {:key :select-with-group}} [:p.error]]] 65 | [:div.plain-field {:free-form/error-class {:key :textarea :error "validation-errors"}} 66 | [:label {:for :text-area} "Text area"] 67 | [:textarea {:free-form/input {:key :textarea} 68 | :id :textarea}] 69 | [:div.errors {:free-form/error-message {:key :textarea}} [:p.error]]] 70 | [:div.plain-field {:free-form/error-class {:key [:t :e :x :t] :error "validation-errors"}} 71 | [:label {:for :text} "Text with deep keys"] 72 | [:input {:free-form/input {:keys [:t :e :x :t]} 73 | :type :text 74 | :id :text 75 | :placeholder "placeholder"}] 76 | [:div.errors {:free-form/error-message {:keys [:t :e :x :t]}} [:p.error]]] 77 | [:div.plain-field {:free-form/error-class {:key :text-with-extra-validation-errors :error "validation-errors" 78 | :extra-keys [[:text] [:-general]]}} 79 | [:label {:for :text-with-extra-validation-errors} "Text with extra validation errors"] 80 | [:input {:free-form/input {:key :text-with-extra-validation-errors} 81 | :type :text 82 | :id :text-with-extra-validation-errors 83 | :placeholder "This will be marked as a validation error also when Text and General have validation errors."}] 84 | [:div.errors {:free-form/error-message {:key :text-with-extra-validation-errors}} [:p.error]]] 85 | [:div {:free-form/error-class {:key :checkbox :error "validation-errors"}} 86 | [:input {:free-form/input {:key :checkbox} 87 | :type :checkbox 88 | :id :checkbox}] 89 | [:label {:for :checkbox} "Checkbox"] 90 | [:div.errors {:free-form/error-message {:key :checkbox}} [:p.error]]] 91 | [:div.plain-field {:free-form/error-class {:key :radio-buttons :error "validation-errors"}} 92 | [:label 93 | [:input {:free-form/input {:key :radio-buttons} 94 | :type :radio 95 | :name :radio-buttons 96 | :value "radio-option-1"}] 97 | "Radio Option 1"] 98 | [:label 99 | [:input {:free-form/input {:key :radio-buttons} 100 | :type :radio 101 | :name :radio-buttons 102 | :value "radio-option-2"}] 103 | "Radio Option 2"] 104 | [:label 105 | [:input {:free-form/input {:key :radio-buttons} 106 | :type :radio 107 | :name :radio-buttons 108 | :value "radio-option-3"}] 109 | "Radio Option 3"] 110 | [:div.errors {:free-form/error-message {:key :radio-buttons}} [:p.error]]] 111 | [:button "Button"]]] 112 | 113 | (testing "simple generation" 114 | (let [generated-input (hide-on-change 115 | (free-form/form {} {} (fn [_keys _value]) 116 | plain-reagent-form-template))] 117 | (is (= generated-input 118 | [:form {:noValidate true} 119 | nil 120 | [:div.plain-field {} 121 | [:label {:for :text} "Text"] 122 | [:input {:type :text 123 | :id :text 124 | :placeholder "placeholder" 125 | :value "" 126 | :on-change :was-function}] 127 | nil] 128 | [:div.plain-field {} 129 | [:label {:for :email} "Email"] 130 | [:input {:type :email 131 | :id :email 132 | :placeholder "placeholder@example.com" 133 | :value "" 134 | :on-change :was-function}] 135 | nil] 136 | [:div.plain-field {} 137 | [:label {:for :password} "Password"] 138 | [:input {:type :password 139 | :id :password 140 | :value "" 141 | :on-change :was-function}] 142 | nil] 143 | [:div.plain-field {} 144 | [:label {:for :select} "Select"] 145 | [:select {:type :select 146 | :id :select 147 | :value "" 148 | :on-change :was-function} 149 | [:option] 150 | [:option {:value :dog} "Dog"] 151 | [:option {:value :cat} "Cat"] 152 | [:option {:value :squirrel} "Squirrel"] 153 | [:option {:value :giraffe} "Giraffe"]] 154 | nil] 155 | [:div.plain-field {} 156 | [:label {:for :select} "Select with groups"] 157 | [:select {:type :select 158 | :id :select-with-group 159 | :value "" 160 | :on-change :was-function} 161 | [:option] 162 | [:optgroup {:label "Numbers"} 163 | [:option {:value :one} "One"] 164 | [:option {:value :two} "Two"] 165 | [:option {:value :three} "Three"] 166 | [:option {:value :four} "Four"]] 167 | [:optgroup {:label "Leters"} 168 | [:option {:value :a} "A"] 169 | [:option {:value :b} "B"] 170 | [:option {:value :c} "C"] 171 | [:option {:value :d} "D"]]] 172 | nil] 173 | [:div.plain-field {} 174 | [:label {:for :text-area} "Text area"] 175 | [:textarea {:id :textarea 176 | :value "" 177 | :on-change :was-function}] 178 | nil] 179 | [:div.plain-field {} 180 | [:label {:for :text} "Text with deep keys"] 181 | [:input {:type :text 182 | :id :text 183 | :placeholder "placeholder" 184 | :value "" 185 | :on-change :was-function}] 186 | nil] 187 | [:div.plain-field {} 188 | [:label {:for :text-with-extra-validation-errors} "Text with extra validation errors"] 189 | [:input {:type :text 190 | :id :text-with-extra-validation-errors 191 | :placeholder "This will be marked as a validation error also when Text and General have validation errors." 192 | :value "" 193 | :on-change :was-function}] 194 | nil] 195 | [:div {} 196 | [:input {:type :checkbox 197 | :id :checkbox 198 | :default-checked false 199 | :on-change :was-function}] 200 | [:label {:for :checkbox} "Checkbox"] 201 | nil] 202 | [:div.plain-field {} 203 | [:label 204 | [:input {:type :radio 205 | :name :radio-buttons 206 | :value "radio-option-1" 207 | :default-checked false 208 | :on-change :was-function}] 209 | "Radio Option 1"] 210 | [:label 211 | [:input {:type :radio 212 | :name :radio-buttons 213 | :value "radio-option-2" 214 | :default-checked false 215 | :on-change :was-function}] 216 | "Radio Option 2"] 217 | [:label 218 | [:input {:type :radio 219 | :name :radio-buttons 220 | :value "radio-option-3" 221 | :default-checked false 222 | :on-change :was-function}] 223 | "Radio Option 3"] 224 | nil] 225 | [:button "Button"]])))) 226 | 227 | (testing "generation with initial data" 228 | (let [generated-input (hide-on-change 229 | (free-form/form {:text "Text value" 230 | :email "Email value" 231 | :password "Password value" 232 | ;:select "cat" ; TODO: enable this and fix generation, as it's broken right now. 233 | ;:select-with-group "two" ; TODO: enable this and fix generation, as it's broken right now. 234 | :textarea "Textarea value" 235 | :t {:e {:x {:t "Text with deep keys value"}}} 236 | :checkbox true 237 | :radio-buttons "radio-option-2" 238 | } {} (fn [_keys _value]) 239 | plain-reagent-form-template))] 240 | (is (= generated-input 241 | [:form {:noValidate true} 242 | nil 243 | [:div.plain-field {} 244 | [:label {:for :text} "Text"] 245 | [:input {:type :text 246 | :id :text 247 | :placeholder "placeholder" 248 | :value "Text value" 249 | :on-change :was-function}] nil] 250 | [:div.plain-field {} 251 | [:label {:for :email} "Email"] 252 | [:input {:type :email 253 | :id :email 254 | :placeholder "placeholder@example.com" 255 | :value "Email value" 256 | :on-change :was-function}] nil] 257 | [:div.plain-field {} 258 | [:label {:for :password} "Password"] 259 | [:input {:type :password 260 | :id :password 261 | :value "Password value" 262 | :on-change :was-function}] nil] 263 | [:div.plain-field {} 264 | [:label {:for :select} "Select"] 265 | [:select {:type :select 266 | :id :select 267 | :value "" 268 | :on-change :was-function} [:option] 269 | [:option {:value :dog} "Dog"] 270 | [:option {:value :cat} "Cat"] 271 | [:option {:value :squirrel} "Squirrel"] 272 | [:option {:value :giraffe} "Giraffe"]] nil] 273 | [:div.plain-field {} 274 | [:label {:for :select} "Select with groups"] 275 | [:select {:type :select 276 | :id :select-with-group 277 | :value "" 278 | :on-change :was-function} [:option] 279 | [:optgroup {:label "Numbers"} [:option {:value :one} "One"] 280 | [:option {:value :two} "Two"] 281 | [:option {:value :three} "Three"] 282 | [:option {:value :four} "Four"]] 283 | [:optgroup {:label "Leters"} [:option {:value :a} "A"] 284 | [:option {:value :b} "B"] 285 | [:option {:value :c} "C"] 286 | [:option {:value :d} "D"]]] nil] 287 | [:div.plain-field {} 288 | [:label {:for :text-area} "Text area"] 289 | [:textarea {:id :textarea 290 | :value "Textarea value" 291 | :on-change :was-function}] nil] 292 | [:div.plain-field {} 293 | [:label {:for :text} "Text with deep keys"] 294 | [:input {:type :text 295 | :id :text 296 | :placeholder "placeholder" 297 | :value "Text with deep keys value" 298 | :on-change :was-function}] nil] 299 | [:div.plain-field {} 300 | [:label {:for :text-with-extra-validation-errors} "Text with extra validation errors"] 301 | [:input {:type :text 302 | :id :text-with-extra-validation-errors 303 | :placeholder "This will be marked as a validation error also when Text and General have validation errors." 304 | :value "" 305 | :on-change :was-function}] nil] 306 | [:div {} 307 | [:input {:type :checkbox 308 | :id :checkbox 309 | :default-checked true 310 | :on-change :was-function}] 311 | [:label {:for :checkbox} "Checkbox"] nil] 312 | [:div.plain-field {} 313 | [:label [:input {:type :radio 314 | :name :radio-buttons 315 | :value "radio-option-1" 316 | :default-checked false 317 | :on-change :was-function}] "Radio Option 1"] 318 | [:label [:input {:type :radio 319 | :name :radio-buttons 320 | :value "radio-option-2" 321 | :default-checked true 322 | :on-change :was-function}] "Radio Option 2"] 323 | [:label [:input {:type :radio 324 | :name :radio-buttons 325 | :value "radio-option-3" 326 | :default-checked false 327 | :on-change :was-function}] "Radio Option 3"] nil] 328 | [:button "Button"]])))))) 329 | --------------------------------------------------------------------------------