├── env
├── prod
│ └── cljs
│ │ └── formap
│ │ └── prod.cljs
└── dev
│ ├── clj
│ └── user.clj
│ └── cljs
│ └── formap
│ └── dev.cljs
├── .gitignore
├── public
├── index.html
├── css
│ └── site.css
└── img
│ └── formap.svg
├── LICENSE
├── project.clj
├── README.md
└── src
└── formap
└── core.cljs
/env/prod/cljs/formap/prod.cljs:
--------------------------------------------------------------------------------
1 | (ns formap.prod
2 | (:require
3 | [formap.core :as core]))
4 |
5 | ;;ignore println statements in prod
6 | (set! *print-fn* (fn [& _]))
7 |
8 | ;(core/init!)
9 |
--------------------------------------------------------------------------------
/env/dev/clj/user.clj:
--------------------------------------------------------------------------------
1 | (ns user
2 | (:require [figwheel-sidecar.repl-api :as ra]))
3 |
4 | (defn start-fw []
5 | (ra/start-figwheel!))
6 |
7 | (defn stop-fw []
8 | (ra/stop-figwheel!))
9 |
10 | (defn cljs []
11 | (ra/cljs-repl))
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | /classes
3 | /checkouts
4 | profiles.clj
5 | pom.xml
6 | pom.xml.asc
7 | *.jar
8 | *.class
9 | /.lein-*
10 | /.nrepl-port
11 | /resources/public/js
12 | /public/js
13 | /out
14 | /.repl
15 | *.log
16 | /.env
17 | .idea
18 | formap.iml
19 | figwheel_server.log
20 | .rebel_readline_history
21 | pom.xml
--------------------------------------------------------------------------------
/env/dev/cljs/formap/dev.cljs:
--------------------------------------------------------------------------------
1 | (ns ^:figwheel-no-load formap.dev
2 | (:require
3 | [formap.core :as core]
4 | [devtools.core :as devtools]))
5 |
6 | (extend-protocol IPrintWithWriter
7 | js/Symbol
8 | (-pr-writer [sym writer _]
9 | (-write writer (str "\"" (.toString sym) "\""))))
10 |
11 | (enable-console-print!)
12 |
13 | (devtools/install!)
14 |
15 | ;(core/init!)
16 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
ClojureScript has not been compiled!
12 |
please run lein figwheel in order to start the compiler
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/public/css/site.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: 'Helvetica Neue', Verdana, Helvetica, Arial, sans-serif;
3 | max-width: 600px;
4 | margin: 0 auto;
5 | padding-top: 72px;
6 | -webkit-font-smoothing: antialiased;
7 | font-size: 1.125em;
8 | color: #333;
9 | line-height: 1.5em;
10 | }
11 |
12 | h1, h2, h3 {
13 | color: #000;
14 | }
15 | h1 {
16 | font-size: 2.5em
17 | }
18 |
19 | h2 {
20 | font-size: 2em
21 | }
22 |
23 | h3 {
24 | font-size: 1.5em
25 | }
26 |
27 | a {
28 | text-decoration: none;
29 | color: #09f;
30 | }
31 |
32 | a:hover {
33 | text-decoration: underline;
34 | }
35 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014 reagent-project
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/project.clj:
--------------------------------------------------------------------------------
1 | (defproject formap "0.1.2"
2 | :description "A reagent library to build awesome dynamic forms."
3 | :url "https://github.com/victorvoid/formap"
4 | :deploy-repositories [["releases" {:sign-releases false :url "https://clojars.org/repo"}]
5 | ["snapshots" {:sign-releases false :url "https://clojars.org/repo"}]]
6 | :license {:name "Eclipse Public License"
7 | :url "http://www.eclipse.org/legal/epl-v10.html"}
8 |
9 | :dependencies [[org.clojure/clojure "1.10.1"]
10 | [org.clojure/clojurescript "1.10.520"]
11 | [reagent "0.8.1"]]
12 |
13 | :plugins [[lein-cljsbuild "1.1.7"]
14 | [lein-figwheel "0.5.19"]]
15 |
16 | :clean-targets ^{:protect false}
17 |
18 | [:target-path
19 | [:cljsbuild :builds :app :compiler :output-dir]
20 | [:cljsbuild :builds :app :compiler :output-to]]
21 |
22 | :resource-paths ["public"]
23 |
24 |
25 | :figwheel {:http-server-root "."
26 | :nrepl-port 7002
27 | :nrepl-middleware [cider.piggieback/wrap-cljs-repl]
28 | :css-dirs ["public/css"]}
29 |
30 | :cljsbuild {:builds {:app
31 | {:source-paths ["src" "env/dev/cljs"]
32 | :compiler
33 | {:main "formap.dev"
34 | :output-to "public/js/app.js"
35 | :output-dir "public/js/out"
36 | :asset-path "js/out"
37 | :source-map true
38 | :optimizations :none
39 | :pretty-print true}
40 | :figwheel
41 | {:on-jsload "formap.core/mount-root"
42 | :open-urls ["http://localhost:3449/index.html"]}}
43 | :release
44 | {:source-paths ["src" "env/prod/cljs"]
45 | :compiler
46 | {:output-to "public/js/app.js"
47 | :output-dir "public/js/release"
48 | :optimizations :advanced
49 | :infer-externs true
50 | :pretty-print false}}}}
51 |
52 | :aliases {"package" ["do" "clean" ["cljsbuild" "once" "release"]]}
53 |
54 | :profiles {:dev {:source-paths ["src" "env/dev/clj"]
55 | :dependencies [[binaryage/devtools "0.9.10"]
56 | [figwheel-sidecar "0.5.19"]
57 | [nrepl "0.6.0"]
58 | [cider/piggieback "0.4.1"]]}})
59 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # formap
2 | [](./LICENSE)
3 |
4 |
5 | 
6 |
7 | A reagent library to build awesome dynamic forms. 🔨
8 |
9 | [](https://clojars.org/formap)
10 |
11 |
12 | ## Installation
13 |
14 | To use formap in an existing project you add this to your dependencies in project.clj
15 |
16 | ```
17 | [formap "0.x.x"]
18 | ```
19 |
20 | ## Why ?
21 |
22 | The main objective is to build a form by a literal map that describe all fields.
23 |
24 | - ♥️ Building form using literal map.
25 | - 🔫 Validators support.
26 | - 📄 Meta class in fields (touched|untouched|valid|invalid|etc).
27 |
28 | ## Documentation
29 |
30 | First you need to create a literal map that describe a form and use it for build.
31 |
32 |
33 | ```cljs
34 | (ns app.pages.signin
35 | (:require
36 | [reagent.core :as r]
37 | [app.utils.validators :refer [username-or-email? password?]]
38 | [formap.core :refer [build-form]]))
39 |
40 | (def signin-fields
41 | {:fields [{:name "login"
42 | :placeholder "Username or Email"
43 | :class "input"
44 | :autoFocus true
45 | :required "Username or Email is required"
46 | :validators [username-or-email?]}
47 |
48 | {:name "password"
49 | :placeholder "Password"
50 | :type "password"
51 | :required "Password is required"
52 | :validators [password?]}]})
53 |
54 | (defn login []
55 | [build-form {:experience signin-fields
56 | :class "myform"
57 | :on-submit #(js/console.log %)} ;;{:login "Text typed" :password "Password typed"}
58 | [:button "Sign in"]])
59 | ```
60 |
61 | 
62 |
63 |
64 | ### Validators
65 | You can create your own validator and set a message error.
66 |
67 | ```cljs
68 | (ns app.pages.signin
69 | (:require
70 | [reagent.core :as r]
71 | [formap.core :refer [build-form]]))
72 |
73 | (defn- match-regex?
74 | "Check if the string matches the regex"
75 | [v regex]
76 | (boolean (re-matches regex v)))
77 |
78 | (defn username-validate
79 | [input]
80 | (if (or (nil? input) (empty? input))
81 | true
82 | (match-regex? input #"^([a-zA-Z0-9.]+@){0,1}([a-zA-Z0-9.])+$")))
83 |
84 | (def username?
85 | {:validate username-validate
86 | :message "Username invalid."})
87 |
88 | (def signin-fields
89 | {:fields [{:name "login"
90 | :placeholder "Username"
91 | :class "input"
92 | :autoFocus true
93 | :required "Username is required"
94 | :validators [username?]}
95 |
96 | {:name "password"
97 | :placeholder "Password"
98 | :type "password"
99 | :required "Password is required"}]})
100 |
101 | (defn login []
102 | [build-form {:experience signin-fields
103 | :class "myform"
104 | :on-submit #(js/console.log %)}
105 | [:button "Sign in"]])
106 | ```
107 |
108 | License
109 | -------
110 |
111 | The code is available under the [MIT License](LICENSE.md).
112 |
--------------------------------------------------------------------------------
/src/formap/core.cljs:
--------------------------------------------------------------------------------
1 | (ns formap.core
2 | (:require
3 | [reagent.core :as r]))
4 |
5 | (defn get-error-messages
6 | "Returns array of errors from validators executed in `value`."
7 | [validators value]
8 | (reduce (fn [acc validator]
9 | (let [fn-validator (:validate validator)
10 | valid? (fn-validator value)]
11 |
12 | (if valid?
13 | acc
14 | (conj acc (:message validator)))))
15 |
16 | [] validators))
17 |
18 | (defn some-field-touched?
19 | "Returns true if some of the `fields` have been touched."
20 | [fields]
21 | (some (fn [[_ value]]
22 | (:touched value)) fields))
23 |
24 | (defn all-fields-valid?
25 | "Returns true if all `fields` are valid."
26 | [fields]
27 | (every? (fn [[_ value]]
28 | (:valid value)) fields))
29 |
30 | (def default-metas
31 | {:touched false :valid false :errors []})
32 |
33 | (defn get-default-meta
34 | "Returns a default meta object according to `fields` name.
35 | {:field-name {:touched false :valid false :errors []}}
36 | "
37 | [fields]
38 | (reduce (fn [acc field]
39 | (let [name (keyword (:name field))]
40 | (merge acc {name default-metas}))) {} fields))
41 |
42 | (defn get-field-meta
43 | "Returns a meta object with dynamic values according to the
44 | validations executed in `form-state`."
45 | [form-state name validators required?]
46 | (let [value (get (:state @form-state) name)
47 | errors (if required?
48 | (or
49 | (when (empty? value) [required?])
50 | (get-error-messages validators value)))
51 |
52 | valid? (empty? errors)
53 | field-meta {:errors errors :valid valid? :touched true}]
54 | field-meta))
55 |
56 | (defn get-input-class
57 | "Returns an array of class name according to the :meta from `form-state`.
58 | :valid/:invalid and :touched/:untouched"
59 | [name form-state]
60 | (let [valid? (if (get-in @form-state [:meta name :valid]) :valid :invalid)
61 | touched? (if (get-in @form-state [:meta name :touched]) :touched :untouched)]
62 | [valid? touched?]))
63 |
64 | (defn on-blur
65 | "Update :meta from `form-state` according to the validations."
66 | [_ form-state {:keys [name validators required]}]
67 | (let [field-meta (get-field-meta form-state name validators required)]
68 | (swap! form-state assoc-in [:meta name] field-meta)))
69 |
70 | (defn on-change
71 | "Update :state from `form-state` to input value and
72 | running validations when form is touched."
73 | [e form-state {:keys [name validators required]}]
74 | (swap! form-state assoc-in [:state name] (-> e .-target .-value))
75 | (let [form-touched? (some-field-touched? (:meta @form-state))
76 | new-meta (get-field-meta form-state name validators required)]
77 | (when form-touched? (swap! form-state assoc-in [:meta name] new-meta))))
78 |
79 | (defn input [attrs form-state field]
80 | (fn []
81 | (let [name (:name attrs)
82 | meta-classes (get-input-class name form-state)
83 | errors (get-in @form-state [:meta name :errors])]
84 |
85 | [:div.field {:class meta-classes}
86 | [:div.field-input
87 | [:input.input (merge attrs {:on-change #(on-change % form-state field)
88 | :value (get-in @form-state [:state name])})]]
89 | [:div.error-wrapper
90 | [:p (first errors)]]])))
91 |
92 | (defn get-custom-elements
93 | "Returns a collection of customized elements according to `fields`."
94 | [fields form-state]
95 | (reduce (fn [acc field]
96 | (let [name (keyword (:name field))
97 | attrs (dissoc field :name :validators)
98 | type (get attrs :type "text")
99 | new-field (assoc field :name name)
100 | custom-attrs {:type type
101 | :key name
102 | :name name
103 | :on-blur #(on-blur % form-state new-field)}
104 | all-attrs (merge attrs custom-attrs)
105 | element [input all-attrs form-state new-field]]
106 |
107 | (conj acc element))) [] fields))
108 |
109 | (defn build-form [{:keys [on-submit experience class]} children]
110 | "Render a form according to the `experience`.
111 | Example:
112 |
113 | (def experience {:fields [{:name 'name'
114 | :placeholder 'Type your name'
115 | :required 'Name is required.'}]})
116 | [cljs-form {:experience experience
117 | :class 'myform'
118 | :on-submit #(js/console.log %)}
119 | [:button 'Send form']]"
120 |
121 | (let [fields (:fields experience)
122 | form-state (r/atom {:state {} :meta (get-default-meta fields)})
123 | elements (get-custom-elements fields form-state)
124 | on-submit (fn [e] (.preventDefault e) (on-submit (:state @form-state)))]
125 |
126 | (fn []
127 | (let [form-valid? (all-fields-valid? (:meta @form-state))
128 | form-touched? (some-field-touched? (:meta @form-state))
129 | form-class (str (if form-valid? " valid" " invalid")
130 | (if form-touched? " touched" " untouched"))]
131 |
132 | [:form {:on-submit on-submit
133 | :class (str class form-class)}
134 |
135 | (for [item elements]
136 | ^{:key item} item)
137 | children]))))
--------------------------------------------------------------------------------
/public/img/formap.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------